mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 00:37:45 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e60ddbe85 | ||
|
|
44334fca52 |
@@ -24,17 +24,18 @@ func (b *Builder) Platform(ctx context.Context, cfg *client.Config) (*v1alpha1.P
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
|
||||
// We only process the first instance, assume the render platform subcommand enforces this.
|
||||
for idx, instance := range instances {
|
||||
log.DebugContext(ctx, "cue: building instance", "idx", idx, "dir", instance.Dir)
|
||||
p, err := b.runPlatform(ctx, instance)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not build platform: %w", err))
|
||||
}
|
||||
return p, nil
|
||||
if len(instances) != 1 {
|
||||
return nil, errors.Wrap(errors.New(fmt.Sprintf("instances length %d must be exactly 1", len(instances))))
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(errors.New("missing platform instance"))
|
||||
// We only process the first instance, assume the render platform subcommand enforces this.
|
||||
instance := instances[0]
|
||||
log.DebugContext(ctx, "cue: building instance", "dir", instance.Dir)
|
||||
p, err := b.runPlatform(ctx, instance)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not build platform: %w", err))
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (b Builder) runPlatform(ctx context.Context, instance *build.Instance) (*v1alpha1.Platform, error) {
|
||||
|
||||
@@ -20,6 +20,7 @@ func New(cfg *holos.Config) *cobra.Command {
|
||||
cmd.Args = cobra.NoArgs
|
||||
|
||||
cmd.AddCommand(NewPlatform(cfg))
|
||||
cmd.AddCommand(NewComponent())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -45,3 +46,44 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewComponent returns a command to generate a holos component
|
||||
func NewComponent() *cobra.Command {
|
||||
cmd := command.New("component")
|
||||
cmd.Short = "generate a component from an embedded schematic"
|
||||
|
||||
cmd.AddCommand(NewCueComponent())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewCueComponent() *cobra.Command {
|
||||
cmd := command.New("cue")
|
||||
cmd.Short = "generate a cue component from an embedded schematic"
|
||||
|
||||
components := generate.CueComponents()
|
||||
cmd.Long = fmt.Sprintf("Embedded cue components available to generate:\n\n %s", strings.Join(components, "\n "))
|
||||
for _, name := range components {
|
||||
cmd.AddCommand(makeCueCommand(name))
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func makeCueCommand(name string) *cobra.Command {
|
||||
cmd := command.New(name)
|
||||
cmd.Short = fmt.Sprintf("generate a %s cue component from an embedded schematic", name)
|
||||
cmd.Args = cobra.NoArgs
|
||||
|
||||
cfg := &generate.CueConfig{}
|
||||
cmd.Flags().AddGoFlagSet(cfg.FlagSet())
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Root().Context()
|
||||
if err := generate.GenerateCueComponent(ctx, name, cfg); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
93
internal/generate/component.go
Normal file
93
internal/generate/component.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/server/middleware/logger"
|
||||
)
|
||||
|
||||
//go:embed all:components
|
||||
var components embed.FS
|
||||
|
||||
// componentsRoot is the root path to copy component cue code from.
|
||||
const componentsRoot = "components"
|
||||
|
||||
// CueConfig represents the config values passed to cue go templates.
|
||||
type CueConfig struct {
|
||||
ComponentName string
|
||||
flagSet *flag.FlagSet
|
||||
}
|
||||
|
||||
func (c *CueConfig) FlagSet() *flag.FlagSet {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if c.flagSet != nil {
|
||||
return c.flagSet
|
||||
}
|
||||
fs := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
fs.StringVar(&c.ComponentName, "name", "example", "component name")
|
||||
c.flagSet = fs
|
||||
return fs
|
||||
}
|
||||
|
||||
// CueComponents returns a slice of embedded component schematics or nil if there are none.
|
||||
func CueComponents() []string {
|
||||
entries, err := fs.ReadDir(components, filepath.Join(componentsRoot, "cue"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
dirs := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
dirs = append(dirs, entry.Name())
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// makeRenderFunc makes a template rendering function for embedded files.
|
||||
func makeRenderFunc(log *slog.Logger, path string, cfg *CueConfig) func([]byte) *bytes.Buffer {
|
||||
return func(content []byte) *bytes.Buffer {
|
||||
tmpl, err := template.New(filepath.Base(path)).Parse(string(content))
|
||||
if err != nil {
|
||||
log.Error("could not load template", "err", err)
|
||||
return bytes.NewBuffer(content)
|
||||
}
|
||||
|
||||
var rendered bytes.Buffer
|
||||
if err := tmpl.Execute(&rendered, cfg); err != nil {
|
||||
log.Error("could not execute template", "err", err)
|
||||
return bytes.NewBuffer(content)
|
||||
}
|
||||
|
||||
return &rendered
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCueComponent writes the cue code for a component to the local working
|
||||
// directory.
|
||||
func GenerateCueComponent(ctx context.Context, name string, cfg *CueConfig) error {
|
||||
path := filepath.Join(componentsRoot, "cue", name)
|
||||
dstPath := filepath.Join(getCwd(ctx), cfg.ComponentName)
|
||||
log := logger.FromContext(ctx).With("name", cfg.ComponentName, "path", dstPath)
|
||||
log.DebugContext(ctx, "mkdir")
|
||||
if err := os.MkdirAll(dstPath, os.ModePerm); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
mapper := makeRenderFunc(log, path, cfg)
|
||||
if err := copyEmbedFS(ctx, components, path, dstPath, mapper); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "generated component")
|
||||
return nil
|
||||
}
|
||||
32
internal/generate/components/cue/minimal/component.cue
Normal file
32
internal/generate/components/cue/minimal/component.cue
Normal file
@@ -0,0 +1,32 @@
|
||||
package holos
|
||||
|
||||
import v1 "github.com/holos-run/holos/api/v1alpha1"
|
||||
import "encoding/yaml"
|
||||
|
||||
let ComponentName = "{{ .ComponentName }}"
|
||||
|
||||
// The BuildPlan represents the kubernetes api objects to manage. CUE returns
|
||||
// the build plan to the holos CLI for rendering to plain yaml files.
|
||||
v1.#BuildPlan & {
|
||||
spec: components: resources: "\(ComponentName)": {
|
||||
metadata: name: ComponentName
|
||||
apiObjectMap: OBJECTS.apiObjectMap
|
||||
}
|
||||
}
|
||||
|
||||
// OBJECTS represents the kubernetes api objects to manage.
|
||||
let OBJECTS = v1.#APIObjects & {
|
||||
// Add Kubernetes API Objects to manage here.
|
||||
apiObjects: ConfigMap: "\(ComponentName)": {
|
||||
metadata: {
|
||||
name: ComponentName
|
||||
namespace: "default"
|
||||
}
|
||||
data: platform: yaml.Marshal(PLATFORM)
|
||||
}
|
||||
}
|
||||
|
||||
// This is an example of how to refer to the Platform model.
|
||||
let PLATFORM = {
|
||||
spec: model: _Platform.spec.model
|
||||
}
|
||||
@@ -1,97 +1,17 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/holos-run/holos/internal/client"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/server/middleware/logger"
|
||||
platform "github.com/holos-run/holos/service/gen/holos/platform/v1alpha1"
|
||||
)
|
||||
|
||||
//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, rpc *client.Client, orgID string, name string) error {
|
||||
log := logger.FromContext(ctx)
|
||||
// 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()))
|
||||
}
|
||||
|
||||
// Link the local platform the SaaS platform ID.
|
||||
rpcPlatforms, err := rpc.Platforms(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
var rpcPlatform *platform.Platform
|
||||
for _, p := range rpcPlatforms {
|
||||
if p.GetName() == name {
|
||||
rpcPlatform = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if rpcPlatform == nil {
|
||||
return errors.Wrap(errors.New("cannot generate: platform not found in the holos server"))
|
||||
}
|
||||
|
||||
// Write the platform data.
|
||||
data, err := json.MarshalIndent(rpcPlatform, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if len(data) > 0 {
|
||||
data = append(data, '\n')
|
||||
}
|
||||
log = log.With("platform_id", rpcPlatform.GetId())
|
||||
if err := os.WriteFile(client.PlatformMetadataFile, data, 0644); err != nil {
|
||||
return errors.Wrap(fmt.Errorf("could not write platform metadata: %w", err))
|
||||
}
|
||||
log.InfoContext(ctx, "wrote "+client.PlatformMetadataFile, "path", filepath.Join(getCwd(ctx), client.PlatformMetadataFile))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "generated platform "+name, "path", getCwd(ctx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirExists(srcFS embed.FS, path string) bool {
|
||||
entries, err := fs.ReadDir(srcFS, path)
|
||||
if err != nil {
|
||||
@@ -100,7 +20,9 @@ func dirExists(srcFS embed.FS, path string) bool {
|
||||
return len(entries) > 0
|
||||
}
|
||||
|
||||
func copyEmbedFS(ctx context.Context, srcFS embed.FS, srcPath, dstPath string) error {
|
||||
// copyEmbedFS copies embedded files from srcPath to dstPath passing the
|
||||
// contents through mapFunc.
|
||||
func copyEmbedFS(ctx context.Context, srcFS embed.FS, srcPath, dstPath string, mapFunc func([]byte) *bytes.Buffer) error {
|
||||
log := logger.FromContext(ctx)
|
||||
return fs.WalkDir(srcFS, srcPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
@@ -124,7 +46,8 @@ func copyEmbedFS(ctx context.Context, srcFS embed.FS, srcPath, dstPath string) e
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if err := os.WriteFile(dstFullPath, data, os.ModePerm); err != nil {
|
||||
buf := mapFunc(data)
|
||||
if err := os.WriteFile(dstFullPath, buf.Bytes(), os.ModePerm); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
log.DebugContext(ctx, "wrote", "file", dstFullPath)
|
||||
|
||||
94
internal/generate/platform.go
Normal file
94
internal/generate/platform.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/holos-run/holos/internal/client"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/logger"
|
||||
platform "github.com/holos-run/holos/service/gen/holos/platform/v1alpha1"
|
||||
)
|
||||
|
||||
//go:embed all:platforms
|
||||
var platforms embed.FS
|
||||
|
||||
// platformsRoot is the root path to copy platform cue code from.
|
||||
const platformsRoot = "platforms"
|
||||
|
||||
// Platforms returns a slice of embedded platforms or nil if there are none.
|
||||
func Platforms() []string {
|
||||
entries, err := fs.ReadDir(platforms, platformsRoot)
|
||||
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, rpc *client.Client, orgID string, name string) error {
|
||||
log := logger.FromContext(ctx)
|
||||
// Check for a valid platform
|
||||
platformPath := filepath.Join(platformsRoot, name)
|
||||
if !dirExists(platforms, platformPath) {
|
||||
return errors.Wrap(fmt.Errorf("cannot generate: have: [%s] want: %+v", name, Platforms()))
|
||||
}
|
||||
|
||||
// Link the local platform the SaaS platform ID.
|
||||
rpcPlatforms, err := rpc.Platforms(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
var rpcPlatform *platform.Platform
|
||||
for _, p := range rpcPlatforms {
|
||||
if p.GetName() == name {
|
||||
rpcPlatform = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if rpcPlatform == nil {
|
||||
return errors.Wrap(errors.New("cannot generate: platform not found in the holos server"))
|
||||
}
|
||||
|
||||
// Write the platform data.
|
||||
data, err := json.MarshalIndent(rpcPlatform, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if len(data) > 0 {
|
||||
data = append(data, '\n')
|
||||
}
|
||||
log = log.With("platform_id", rpcPlatform.GetId())
|
||||
if err := os.WriteFile(client.PlatformMetadataFile, data, 0644); err != nil {
|
||||
return errors.Wrap(fmt.Errorf("could not write platform metadata: %w", err))
|
||||
}
|
||||
log.InfoContext(ctx, "wrote "+client.PlatformMetadataFile, "path", filepath.Join(getCwd(ctx), client.PlatformMetadataFile))
|
||||
|
||||
// Copy the cue.mod directory
|
||||
if err := copyEmbedFS(ctx, platforms, filepath.Join(platformsRoot, "cue.mod"), "cue.mod", bytes.NewBuffer); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// Copy the named platform
|
||||
if err := copyEmbedFS(ctx, platforms, platformPath, ".", bytes.NewBuffer); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "generated platform "+name, "path", getCwd(ctx))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func Platform(ctx context.Context, pf *v1alpha1.Platform, stderr io.Writer) erro
|
||||
args := []string{"render", "component", "--cluster-name", component.Cluster, component.Path}
|
||||
result, err := util.RunCmd(ctx, "holos", args...)
|
||||
if err != nil {
|
||||
io.Copy(stderr, result.Stderr)
|
||||
_, _ = io.Copy(stderr, result.Stderr)
|
||||
return errors.Wrap(fmt.Errorf("could not render component: %w", err))
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
@@ -1 +1 @@
|
||||
80
|
||||
81
|
||||
|
||||
@@ -1 +1 @@
|
||||
2
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user