Compare commits

..

3 Commits

Author SHA1 Message Date
Jeff McCune
5860c5747b (#87) generate sub-command with embedded platform
This patch adds a generate subcommand that copies a platform embedded
into the executable to the local filesystem.  The purpose is to
accelerate initial setup with canned example platforms.

Two platforms are intended to start, one bare and one reference
platform.  The number of platforms embedded into holos should be kept
small (2-3) to limit our support burden.
2024-05-14 15:03:21 -07:00
Jeff McCune
d3c2d55706 (#172) Deploy v0.76.0 to dev 2024-05-14 13:28:19 -07:00
Jeff McCune
ac2ff47a9c (#172) Wire Version Info in the UI
This patch adds the GetVersion rpc method to
holos.system.v1alpha1.SystemService and wires the version information up
to the Web UI.

This is a good example to crib from later regarding fetching and
refreshing data from the web ui using grpc and field masks.
2024-05-14 11:50:06 -07:00
106 changed files with 1030 additions and 112 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ coverage.out
*.hold/
/deploy/
.vscode/
tmp/

View File

@@ -34,7 +34,7 @@ let OBJECTS = #APIObjects & {
containers: [
{
name: Holos
image: "271053619184.dkr.ecr.us-east-2.amazonaws.com/holos-run/holos-server/holos:0.74.0"
image: "271053619184.dkr.ecr.us-east-2.amazonaws.com/holos-run/holos-server/holos:v0.76.0"
imagePullPolicy: "Always"
env: [
{

2
go.mod
View File

@@ -20,6 +20,7 @@ require (
github.com/lmittmann/tint v1.0.4
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.15
github.com/mennanov/fieldmask-utils v1.1.2
github.com/olekukonko/tablewriter v0.0.5
github.com/prometheus/client_golang v1.19.0
github.com/rogpeppe/go-internal v1.12.0
@@ -157,7 +158,6 @@ require (
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mennanov/fieldmask-utils v1.1.2 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect

View File

@@ -0,0 +1,43 @@
package generate
import (
"fmt"
"strings"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/generate"
"github.com/holos-run/holos/internal/holos"
"github.com/spf13/cobra"
)
// New returns a new generate command.
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("generate")
cmd.Aliases = []string{"gen"}
cmd.Short = "generate local resources"
cmd.Args = cobra.NoArgs
cmd.AddCommand(NewPlatform(cfg))
return cmd
}
func NewPlatform(cfg *holos.Config) *cobra.Command {
cmd := command.New("platform")
cmd.Use = "platform [flags] PLATFORM"
cmd.Short = "generate a platform from an embedded schematic"
cmd.Long = fmt.Sprintf("Embedded platforms available to generate:\n\n %s", strings.Join(generate.Platforms(), "\n "))
cmd.Args = cobra.ExactArgs(1)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
for _, name := range args {
if err := generate.GeneratePlatform(ctx, name); err != nil {
return errors.Wrap(err)
}
}
return nil
}
return cmd
}

View File

@@ -5,11 +5,16 @@ import (
"github.com/spf13/cobra"
"github.com/holos-run/holos/version"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server"
"github.com/holos-run/holos/internal/cli/build"
"github.com/holos-run/holos/internal/cli/controller"
"github.com/holos-run/holos/internal/cli/create"
"github.com/holos-run/holos/internal/cli/generate"
"github.com/holos-run/holos/internal/cli/get"
"github.com/holos-run/holos/internal/cli/kv"
"github.com/holos-run/holos/internal/cli/login"
@@ -19,9 +24,6 @@ import (
"github.com/holos-run/holos/internal/cli/rpc"
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/version"
)
// New returns a new root *cobra.Command for command line execution.
@@ -41,7 +43,7 @@ func New(cfg *holos.Config) *cobra.Command {
return err
}
log := cfg.Logger()
c.SetContext(logger.NewContext(c.Context(), log))
c.Root().SetContext(logger.NewContext(c.Context(), log))
// Set the default logger after flag parsing.
slog.SetDefault(log)
return nil
@@ -65,6 +67,7 @@ func New(cfg *holos.Config) *cobra.Command {
rootCmd.AddCommand(logout.New(cfg))
rootCmd.AddCommand(token.New(cfg))
rootCmd.AddCommand(rpc.New(cfg))
rootCmd.AddCommand(generate.New(cfg))
// Maybe not needed?
rootCmd.AddCommand(txtar.New(cfg))

View File

@@ -10,6 +10,7 @@ import { UserService } from './gen/holos/user/v1alpha1/user_service_connect';
import { OrganizationService } from './gen/holos/organization/v1alpha1/organization_service_connect';
import { PlatformService } from './gen/holos/platform/v1alpha1/platform_service_connect';
import { HolosPanelWrapperComponent } from '../wrappers/holos-panel-wrapper/holos-panel-wrapper.component';
import { SystemService } from './gen/holos/system/v1alpha1/system_service_connect';
export const appConfig: ApplicationConfig = {
providers: [
@@ -19,6 +20,7 @@ export const appConfig: ApplicationConfig = {
provideClient(UserService),
provideClient(OrganizationService),
provideClient(PlatformService),
provideClient(SystemService),
importProvidersFrom(
ConnectModule.forRoot({
baseUrl: window.location.origin

View File

@@ -0,0 +1,81 @@
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
// @generated from file holos/system/v1alpha1/system.proto (package holos.system.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
* @generated from message holos.system.v1alpha1.Version
*/
export class Version extends Message<Version> {
/**
* @generated from field: string version = 1;
*/
version = "";
/**
* @generated from field: string git_commit = 2;
*/
gitCommit = "";
/**
* @generated from field: string git_tree_state = 3;
*/
gitTreeState = "";
/**
* @generated from field: string go_version = 4;
*/
goVersion = "";
/**
* @generated from field: string build_date = 5;
*/
buildDate = "";
/**
* @generated from field: string os = 6;
*/
os = "";
/**
* @generated from field: string arch = 7;
*/
arch = "";
constructor(data?: PartialMessage<Version>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.system.v1alpha1.Version";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "git_commit", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "git_tree_state", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "go_version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "build_date", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 6, name: "os", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 7, name: "arch", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Version {
return new Version().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Version {
return new Version().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Version {
return new Version().fromJsonString(jsonString, options);
}
static equals(a: Version | PlainMessage<Version> | undefined, b: Version | PlainMessage<Version> | undefined): boolean {
return proto3.util.equals(Version, a, b);
}
}

View File

@@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { DropTablesRequest, DropTablesResponse, SeedDatabaseRequest, SeedDatabaseResponse } from "./system_service_pb.js";
import { DropTablesRequest, DropTablesResponse, GetVersionRequest, GetVersionResponse, SeedDatabaseRequest, SeedDatabaseResponse } from "./system_service_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
@@ -13,12 +13,12 @@ export const SystemService = {
typeName: "holos.system.v1alpha1.SystemService",
methods: {
/**
* @generated from rpc holos.system.v1alpha1.SystemService.SeedDatabase
* @generated from rpc holos.system.v1alpha1.SystemService.GetVersion
*/
seedDatabase: {
name: "SeedDatabase",
I: SeedDatabaseRequest,
O: SeedDatabaseResponse,
getVersion: {
name: "GetVersion",
I: GetVersionRequest,
O: GetVersionResponse,
kind: MethodKind.Unary,
},
/**
@@ -30,6 +30,15 @@ export const SystemService = {
O: DropTablesResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.system.v1alpha1.SystemService.SeedDatabase
*/
seedDatabase: {
name: "SeedDatabase",
I: SeedDatabaseRequest,
O: SeedDatabaseResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -4,7 +4,84 @@
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
import { FieldMask, Message, proto3 } from "@bufbuild/protobuf";
import { Version } from "./system_pb.js";
/**
* @generated from message holos.system.v1alpha1.GetVersionRequest
*/
export class GetVersionRequest extends Message<GetVersionRequest> {
/**
* FieldMask represents the fields to include in the response.
*
* @generated from field: google.protobuf.FieldMask field_mask = 1;
*/
fieldMask?: FieldMask;
constructor(data?: PartialMessage<GetVersionRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.system.v1alpha1.GetVersionRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "field_mask", kind: "message", T: FieldMask },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVersionRequest {
return new GetVersionRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVersionRequest {
return new GetVersionRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVersionRequest {
return new GetVersionRequest().fromJsonString(jsonString, options);
}
static equals(a: GetVersionRequest | PlainMessage<GetVersionRequest> | undefined, b: GetVersionRequest | PlainMessage<GetVersionRequest> | undefined): boolean {
return proto3.util.equals(GetVersionRequest, a, b);
}
}
/**
* @generated from message holos.system.v1alpha1.GetVersionResponse
*/
export class GetVersionResponse extends Message<GetVersionResponse> {
/**
* @generated from field: holos.system.v1alpha1.Version version = 1;
*/
version?: Version;
constructor(data?: PartialMessage<GetVersionResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.system.v1alpha1.GetVersionResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "version", kind: "message", T: Version },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVersionResponse {
return new GetVersionResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVersionResponse {
return new GetVersionResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVersionResponse {
return new GetVersionResponse().fromJsonString(jsonString, options);
}
static equals(a: GetVersionResponse | PlainMessage<GetVersionResponse> | undefined, b: GetVersionResponse | PlainMessage<GetVersionResponse> | undefined): boolean {
return proto3.util.equals(GetVersionResponse, a, b);
}
}
/**
* @generated from message holos.system.v1alpha1.SeedDatabaseRequest

View File

@@ -31,6 +31,7 @@
</button>
}
</span>
<app-version-button></app-version-button>
<app-profile-button [user$]="user$"></app-profile-button>
</mat-toolbar>
<main class="main-content">

View File

@@ -1,20 +1,21 @@
import { Component, OnInit, inject } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { AsyncPipe, NgIf } from '@angular/common';
import { MatToolbarModule } from '@angular/material/toolbar';
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { ProfileButtonComponent } from '../profile-button/profile-button.component';
import { User } from '../gen/holos/user/v1alpha1/user_pb';
import { UserService } from '../services/user.service';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { map, shareReplay, takeUntil } from 'rxjs/operators';
import { Organization } from '../gen/holos/organization/v1alpha1/organization_pb';
import { User } from '../gen/holos/user/v1alpha1/user_pb';
import { ProfileButtonComponent } from '../profile-button/profile-button.component';
import { OrganizationService } from '../services/organization.service';
import { UserService } from '../services/user.service';
import { VersionButtonComponent } from '../version-button/version-button.component';
@Component({
selector: 'app-nav',
@@ -34,28 +35,35 @@ import { OrganizationService } from '../services/organization.service';
RouterOutlet,
MatCardModule,
ProfileButtonComponent,
VersionButtonComponent,
]
})
export class NavComponent implements OnInit {
export class NavComponent implements OnInit, OnDestroy {
private breakpointObserver = inject(BreakpointObserver);
private userService = inject(UserService);
private orgService = inject(OrganizationService);
private destroy$: Subject<boolean> = new Subject<boolean>();
user$!: Observable<User | null>;
org$!: Observable<Organization | undefined>;
refreshOrg(): void {
this.orgService.refreshOrganizations()
}
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches),
shareReplay()
);
refreshOrg(): void {
this.orgService.refreshOrganizations()
}
ngOnInit(): void {
this.user$ = this.userService.getUser();
this.org$ = this.orgService.activeOrg();
this.user$ = this.userService.getUser().pipe(takeUntil(this.destroy$));
this.org$ = this.orgService.activeOrg().pipe(takeUntil(this.destroy$));
}
public ngOnDestroy(): void {
this.destroy$.next(true);
this.destroy$.complete();
}
}

View File

@@ -1,11 +1,11 @@
import { Inject, Injectable } from '@angular/core';
import { FieldMask, JsonValue, Struct } from '@bufbuild/protobuf';
import { Observable, of, switchMap } from 'rxjs';
import { ObservableClient } from '../../connect/observable-client';
import { Organization } from '../gen/holos/organization/v1alpha1/organization_pb';
import { Platform } from '../gen/holos/platform/v1alpha1/platform_pb';
import { PlatformService as ConnectPlatformService } from '../gen/holos/platform/v1alpha1/platform_service_connect';
import { Platform, Spec } from '../gen/holos/platform/v1alpha1/platform_pb';
import { GetPlatformRequest, ListPlatformsRequest, UpdatePlatformOperation, UpdatePlatformRequest } from '../gen/holos/platform/v1alpha1/platform_service_pb';
import { FieldMask, JsonValue, Struct } from '@bufbuild/protobuf';
@Injectable({
providedIn: 'root'

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SystemService } from './system.service';
describe('SystemService', () => {
let service: SystemService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SystemService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
import { Inject, Injectable } from '@angular/core';
import { Observable, of, switchMap } from 'rxjs';
import { ObservableClient } from '../../connect/observable-client';
import { Version } from '../gen/holos/system/v1alpha1/system_pb';
import { SystemService as ConnectSystemService } from '../gen/holos/system/v1alpha1/system_service_connect';
import { GetVersionRequest } from '../gen/holos/system/v1alpha1/system_service_pb';
import { FieldMask } from '@bufbuild/protobuf';
@Injectable({
providedIn: 'root'
})
export class SystemService {
getVersion(): Observable<Version | undefined> {
const fieldMask = new FieldMask({ paths: ["version", "git_commit", "go_version", "os", "arch"] })
const req = new GetVersionRequest({ fieldMask: fieldMask })
return this.client.getVersion(req).pipe(
switchMap(resp => { return of(resp.version) })
)
}
constructor(@Inject(ConnectSystemService) private client: ObservableClient<typeof ConnectSystemService>) { }
}

View File

@@ -0,0 +1,8 @@
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
it('create an instance', () => {
const pipe = new TruncatePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 8): string {
if (!value) return '';
return value.length > limit ? value.substring(0, limit) : value;
}
}

View File

@@ -0,0 +1,23 @@
@if (version$ | async; as version) {
<button mat-button [matMenuTriggerFor]="menu">
{{ version.version }}
</button>
<mat-menu class="version-menu" #menu="matMenu">
<mat-card class="version-card">
<mat-card-header>
<mat-card-title>{{ version.version }}</mat-card-title>
<mat-card-subtitle>Server version info</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<pre>Git: {{ version.gitCommit | truncate }}</pre>
<pre>Go: {{ version.goVersion | truncate }}</pre>
<pre>OS: {{ version.os | truncate }}</pre>
<pre>Arch: {{ version.arch | truncate }}</pre>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="refreshVersion()" [disabled]="isLoading">Refresh</button>
</mat-card-actions>
</mat-card>
</mat-menu>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VersionButtonComponent } from './version-button.component';
describe('VersionButtonComponent', () => {
let component: VersionButtonComponent;
let fixture: ComponentFixture<VersionButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VersionButtonComponent]
})
.compileComponents();
fixture = TestBed.createComponent(VersionButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,58 @@
import { AsyncPipe, NgIf, NgStyle } from '@angular/common';
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subject, of, startWith, switchMap, takeUntil } from 'rxjs';
import { Version } from '../gen/holos/system/v1alpha1/system_pb';
import { SystemService } from '../services/system.service';
import { TruncatePipe } from '../truncate.pipe';
import { MatDivider } from '@angular/material/divider';
@Component({
selector: 'app-version-button',
standalone: true,
imports: [
AsyncPipe,
MatButtonModule,
MatCardModule,
MatDivider,
MatIconModule,
MatMenuModule,
NgIf,
NgStyle,
TruncatePipe,
],
templateUrl: './version-button.component.html',
styleUrl: './version-button.component.scss'
})
export class VersionButtonComponent implements OnInit, OnDestroy {
private destroy$: Subject<boolean> = new Subject<boolean>();
private refreshVersion$ = new Subject<boolean>();
private systemService = inject(SystemService);
version$!: Observable<Version | undefined>;
isLoading = false;
refreshVersion(): void {
this.refreshVersion$.next(true);
}
ngOnInit(): void {
this.version$ = this.refreshVersion$.pipe(
takeUntil(this.destroy$),
startWith(true),
switchMap(() => {
this.isLoading = true;
return this.systemService.getVersion().pipe(
switchMap((version) => { this.isLoading = false; return of(version); })
);
}),
)
}
public ngOnDestroy(): void {
this.destroy$.next(true);
this.destroy$.complete();
}
}

View File

@@ -3,14 +3,14 @@ import { Component, Input, OnDestroy, inject } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatDivider } from '@angular/material/divider';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { JsonValue } from '@bufbuild/protobuf';
import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core';
import { FormlyMaterialModule } from '@ngx-formly/material';
import { Subject, takeUntil } from 'rxjs';
import { PlatformService } from '../../services/platform.service';
import { Platform } from '../../gen/holos/platform/v1alpha1/platform_pb';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { PlatformService } from '../../services/platform.service';
@Component({
selector: 'app-platform-detail',

View File

@@ -0,0 +1,97 @@
package generate
import (
"context"
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/logger"
)
//go:embed all:platforms
var platforms embed.FS
// root is the root path to copy platform cue code from.
const root = "platforms"
// Platforms returns a slice of embedded platforms or nil if there are none.
func Platforms() []string {
entries, err := fs.ReadDir(platforms, root)
if err != nil {
return nil
}
dirs := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() && entry.Name() != "cue.mod" {
dirs = append(dirs, entry.Name())
}
}
return dirs
}
// GeneratePlatform writes the cue code for a platform to the local working
// directory.
func GeneratePlatform(ctx context.Context, name string) error {
// Check for a valid platform
platformPath := filepath.Join(root, name)
if !dirExists(platforms, platformPath) {
return errors.Wrap(fmt.Errorf("cannot generate: have: [%s] want: %+v", name, Platforms()))
}
// Copy the cue.mod directory
if err := copyEmbedFS(ctx, platforms, filepath.Join(root, "cue.mod"), "cue.mod"); err != nil {
return errors.Wrap(err)
}
// Copy the named platform
if err := copyEmbedFS(ctx, platforms, platformPath, "."); err != nil {
return errors.Wrap(err)
}
return nil
}
func dirExists(srcFS embed.FS, path string) bool {
entries, err := fs.ReadDir(srcFS, path)
if err != nil {
return false
}
return len(entries) > 0
}
func copyEmbedFS(ctx context.Context, srcFS embed.FS, srcPath, dstPath string) error {
log := logger.FromContext(ctx)
return fs.WalkDir(srcFS, srcPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return errors.Wrap(err)
}
relPath, err := filepath.Rel(srcPath, path)
if err != nil {
return errors.Wrap(err)
}
dstFullPath := filepath.Join(dstPath, relPath)
if d.IsDir() {
if err := os.MkdirAll(dstFullPath, os.ModePerm); err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, "created", "directory", dstFullPath)
} else {
data, err := srcFS.ReadFile(path)
if err != nil {
return errors.Wrap(err)
}
if err := os.WriteFile(dstFullPath, data, os.ModePerm); err != nil {
return errors.Wrap(err)
}
log.DebugContext(ctx, "wrote", "file", dstFullPath)
}
return nil
})
}

View File

@@ -73,7 +73,7 @@ func (h *PlatformHandler) GetPlatform(ctx context.Context, req *connect.Request[
}
rpcPlatform := PlatformToRPC(dbPlatform)
mask, err := fieldmask_utils.MaskFromPaths(req.Msg.GetFieldMask().GetPaths(), strings.PascalCase)
mask, err := fieldmask_utils.MaskFromProtoFieldMask(req.Msg.GetFieldMask(), strings.PascalCase)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
@@ -95,7 +95,7 @@ func (h *PlatformHandler) ListPlatforms(ctx context.Context, req *connect.Reques
return nil, errors.Wrap(err)
}
mask, err := fieldmask_utils.MaskFromPaths(req.Msg.GetFieldMask().GetPaths(), strings.PascalCase)
mask, err := fieldmask_utils.MaskFromProtoFieldMask(req.Msg.GetFieldMask(), strings.PascalCase)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
@@ -348,7 +348,9 @@ func rpcPlatforms(reqDBOrg *ent.Organization, mask fieldmask_utils.Mask) ([]*pla
// fieldmask_utils.StructToMap. This is here largely as an placeholder to
// remember we can mutate the value if we want.
func newVisitor(ctx context.Context) func(filter fieldmask_utils.FieldFilter, src, dst reflect.Value, srcFieldName, dstFieldName string, srcFieldValue reflect.Value) fieldmask_utils.MapVisitorResult {
log := logger.FromContext(ctx)
return func(filter fieldmask_utils.FieldFilter, src, dst reflect.Value, srcFieldName, dstFieldName string, srcFieldValue reflect.Value) fieldmask_utils.MapVisitorResult {
log.DebugContext(ctx, "visitor", "srcFieldName", srcFieldName, "dstFieldName", dstFieldName)
return fieldmask_utils.MapVisitorResult{
UpdatedDst: &dst,
}

View File

@@ -13,8 +13,11 @@ import (
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
"github.com/holos-run/holos/internal/server/middleware/logger"
holosstrings "github.com/holos-run/holos/internal/strings"
storage "github.com/holos-run/holos/service/gen/holos/storage/v1alpha1"
system "github.com/holos-run/holos/service/gen/holos/system/v1alpha1"
"github.com/holos-run/holos/version"
fieldmask_utils "github.com/mennanov/fieldmask-utils"
)
const AdminEmail = "jeff@openinfrastructure.co"
@@ -41,6 +44,26 @@ func (h *SystemHandler) checkAdmin(ctx context.Context) error {
return nil
}
func (h *SystemHandler) GetVersion(ctx context.Context, req *connect.Request[system.GetVersionRequest]) (*connect.Response[system.GetVersionResponse], error) {
_, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
mask, err := fieldmask_utils.MaskFromProtoFieldMask(req.Msg.GetFieldMask(), holosstrings.PascalCase)
if err != nil {
return nil, errors.Wrap(err)
}
srcVersion := version.NewVersionInfo()
var rpcVersion system.Version
if err := fieldmask_utils.StructToStruct(mask, &srcVersion, &rpcVersion); err != nil {
return nil, errors.Wrap(err)
}
return connect.NewResponse(&system.GetVersionResponse{Version: &rpcVersion}), nil
}
func (h *SystemHandler) DropTables(ctx context.Context, req *connect.Request[system.DropTablesRequest]) (*connect.Response[system.DropTablesResponse], error) {
if err := h.checkAdmin(ctx); err != nil {
return nil, err

Some files were not shown because too many files have changed in this diff Show More