mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 16:54:58 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac7aec0ba | ||
|
|
42f916af41 |
@@ -39,3 +39,14 @@ func (bp *BuildPlan) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bp *BuildPlan) ResultCapacity() (count int) {
|
||||
if bp == nil {
|
||||
return 0
|
||||
}
|
||||
count = len(bp.Spec.Components.HelmChartList) +
|
||||
len(bp.Spec.Components.KubernetesObjectsList) +
|
||||
len(bp.Spec.Components.KustomizeBuildList) +
|
||||
len(bp.Spec.Components.Resources)
|
||||
return count
|
||||
}
|
||||
|
||||
9
api/v1alpha1/platform.go
Normal file
9
api/v1alpha1/platform.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package v1alpha1
|
||||
|
||||
// Platform represents a platform to manage. A Platform resource tells holos
|
||||
// which components to build. The primary use case is to specify the cluster
|
||||
// names, cluster types, and holos components to build.
|
||||
type Platform struct {
|
||||
TypeMeta `json:",inline" yaml:",inline"`
|
||||
Metadata ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
}
|
||||
@@ -19,6 +19,14 @@ type Result struct {
|
||||
accumulatedOutput string
|
||||
}
|
||||
|
||||
// Continue returns true if Skip is true indicating the result is to be skipped over.
|
||||
func (r *Result) Continue() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
return r.Skip
|
||||
}
|
||||
|
||||
func (r *Result) Name() string {
|
||||
return r.Metadata.Name
|
||||
}
|
||||
@@ -32,6 +40,11 @@ func (r *Result) KustomizationFilename(writeTo string, cluster string) string {
|
||||
return filepath.Join(writeTo, "clusters", cluster, "holos", "components", r.Metadata.Name+"-kustomization.gen.yaml")
|
||||
}
|
||||
|
||||
// KustomizationContent returns the kustomization file contents to write.
|
||||
func (r *Result) KustomizationContent() string {
|
||||
return r.KsContent
|
||||
}
|
||||
|
||||
// AccumulatedOutput returns the accumulated rendered output.
|
||||
func (r *Result) AccumulatedOutput() string {
|
||||
return r.accumulatedOutput
|
||||
|
||||
@@ -8,3 +8,13 @@ type TypeMeta struct {
|
||||
func (tm *TypeMeta) GetKind() string {
|
||||
return tm.Kind
|
||||
}
|
||||
|
||||
func (tm *TypeMeta) GetAPIVersion() string {
|
||||
return tm.Kind
|
||||
}
|
||||
|
||||
// Discriminator is an interface to discriminate the kind api object.
|
||||
type Discriminator interface {
|
||||
GetKind() string
|
||||
GetAPIVersion() string
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ _IngressAuthProxy: {
|
||||
spec: {
|
||||
securityContext: seccompProfile: type: "RuntimeDefault"
|
||||
containers: [{
|
||||
image: "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0"
|
||||
// image: "quay.io/oauth3-proxy/oauth2-proxy:v7.6.0"
|
||||
image: "quay.io/holos/oauth2-proxy:v7.6.0-1-g77a03ae2"
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
name: "oauth2-proxy"
|
||||
volumeMounts: [{
|
||||
|
||||
273
internal/builder/builder.go
Normal file
273
internal/builder/builder.go
Normal file
@@ -0,0 +1,273 @@
|
||||
// Package builder is responsible for building fully rendered kubernetes api
|
||||
// objects from various input directories. A directory may contain a platform
|
||||
// spec or a component spec.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/load"
|
||||
"github.com/holos-run/holos/api/v1alpha1"
|
||||
|
||||
"github.com/holos-run/holos"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
KubernetesObjects = v1alpha1.KubernetesObjectsKind
|
||||
// Helm is the value of the kind field of holos build output indicating helm
|
||||
// values and helm command information.
|
||||
Helm = v1alpha1.HelmChartKind
|
||||
// Skip is the value when the instance should be skipped
|
||||
Skip = "Skip"
|
||||
// KustomizeBuild is the value of the kind field of cue output indicating holos should process the component using kustomize build to render output.
|
||||
KustomizeBuild = v1alpha1.KustomizeBuildKind
|
||||
)
|
||||
|
||||
// An Option configures a Builder
|
||||
type Option func(*config)
|
||||
|
||||
type config struct {
|
||||
args []string
|
||||
cluster string
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
cfg config
|
||||
}
|
||||
|
||||
// New returns a new *Builder configured by opts Option.
|
||||
func New(opts ...Option) *Builder {
|
||||
var cfg config
|
||||
for _, f := range opts {
|
||||
f(&cfg)
|
||||
}
|
||||
b := &Builder{cfg: cfg}
|
||||
return b
|
||||
}
|
||||
|
||||
// Entrypoints configures the leaf directories Builder builds.
|
||||
func Entrypoints(args []string) Option {
|
||||
return func(cfg *config) { cfg.args = args }
|
||||
}
|
||||
|
||||
// Cluster configures the cluster name for the holos component instance.
|
||||
func Cluster(name string) Option {
|
||||
return func(cfg *config) { cfg.cluster = name }
|
||||
}
|
||||
|
||||
// Cluster returns the cluster name of the component instance being built.
|
||||
func (b *Builder) Cluster() string {
|
||||
return b.cfg.cluster
|
||||
}
|
||||
|
||||
// Instances returns the cue build instances being built.
|
||||
func (b *Builder) Instances(ctx context.Context) ([]*build.Instance, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
mod, err := b.findCueMod()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
dir := string(mod)
|
||||
|
||||
cfg := load.Config{Dir: dir}
|
||||
|
||||
// Make args relative to the module directory
|
||||
args := make([]string, len(b.cfg.args))
|
||||
for idx, path := range b.cfg.args {
|
||||
target, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not find absolute path: %w", err))
|
||||
}
|
||||
relPath, err := filepath.Rel(dir, target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("invalid argument, must be relative to cue.mod: %w", err))
|
||||
}
|
||||
relPath = "./" + relPath
|
||||
args[idx] = relPath
|
||||
equiv := fmt.Sprintf("cue export --out yaml -t cluster=%v %v", b.Cluster(), relPath)
|
||||
log.Debug("cue: equivalent command: " + equiv)
|
||||
}
|
||||
|
||||
// Refer to https://github.com/cue-lang/cue/blob/v0.7.0/cmd/cue/cmd/common.go#L429
|
||||
cfg.Tags = append(cfg.Tags, "cluster="+b.Cluster())
|
||||
log.DebugContext(ctx, fmt.Sprintf("cue: tags %v", cfg.Tags))
|
||||
|
||||
return load.Instances(args, &cfg), nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ctx context.Context) (results []*v1alpha1.Result, err error) {
|
||||
log := logger.FromContext(ctx)
|
||||
log.DebugContext(ctx, "cue: building instances")
|
||||
instances, err := b.Instances(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = make([]*v1alpha1.Result, 0, len(instances)*8)
|
||||
|
||||
// Each CUE instance provides a BuildPlan
|
||||
for idx, instance := range instances {
|
||||
log.DebugContext(ctx, "cue: building instance", "idx", idx, "dir", instance.Dir)
|
||||
r, err := b.runInstance(ctx, instance)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not run: %w", err))
|
||||
}
|
||||
results = append(results, r...)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (b Builder) runInstance(ctx context.Context, instance *build.Instance) (results []*v1alpha1.Result, err error) {
|
||||
path := holos.InstancePath(instance.Dir)
|
||||
log := logger.FromContext(ctx).With("dir", path)
|
||||
|
||||
if err := instance.Err; err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not load: %w", err))
|
||||
}
|
||||
cueCtx := cuecontext.New()
|
||||
value := cueCtx.BuildInstance(instance)
|
||||
if err := value.Err(); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not build %s: %w", instance.Dir, err))
|
||||
}
|
||||
log.DebugContext(ctx, "cue: validating instance")
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not validate: %w", err))
|
||||
}
|
||||
|
||||
log.DebugContext(ctx, "cue: decoding holos build plan")
|
||||
jsonBytes, err := value.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not marshal cue instance %s: %w", instance.Dir, err))
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||
// Discriminate the type of build plan.
|
||||
tm := &v1alpha1.TypeMeta{}
|
||||
err = decoder.Decode(tm)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("invalid BuildPlan: %s: %w", instance.Dir, err))
|
||||
}
|
||||
|
||||
log.DebugContext(ctx, "cue: discriminated build kind: "+tm.Kind, "kind", tm.Kind, "apiVersion", tm.APIVersion)
|
||||
|
||||
// New decoder for the full object
|
||||
decoder = json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
switch tm.Kind {
|
||||
case "BuildPlan":
|
||||
var bp v1alpha1.BuildPlan
|
||||
if err = decoder.Decode(&bp); err != nil {
|
||||
err = errors.Wrap(fmt.Errorf("could not decode BuildPlan %s: %w", instance.Dir, err))
|
||||
return
|
||||
}
|
||||
results, err = b.buildPlan(ctx, &bp, path)
|
||||
case "Platform":
|
||||
var pf v1alpha1.Platform
|
||||
if err = decoder.Decode(&pf); err != nil {
|
||||
err = errors.Wrap(fmt.Errorf("could not decode Platform %s: %w", instance.Dir, err))
|
||||
return
|
||||
}
|
||||
results, err = b.buildPlatform(ctx, &pf)
|
||||
default:
|
||||
err = errors.Wrap(fmt.Errorf("unknown kind: %v", tm.Kind))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Builder) buildPlatform(ctx context.Context, pf *v1alpha1.Platform) (results []*v1alpha1.Result, err error) {
|
||||
log := logger.FromContext(ctx)
|
||||
log.ErrorContext(ctx, "not implemented", "platform", pf)
|
||||
return nil, errors.Wrap(fmt.Errorf("not implemeneted"))
|
||||
}
|
||||
|
||||
func (b *Builder) buildPlan(ctx context.Context, buildPlan *v1alpha1.BuildPlan, path holos.InstancePath) (results []*v1alpha1.Result, err error) {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
if err := buildPlan.Validate(); err != nil {
|
||||
log.WarnContext(ctx, "could not validate", "skipped", true, "err", err)
|
||||
return nil, errors.Wrap(fmt.Errorf("could not validate %w", err))
|
||||
}
|
||||
|
||||
if buildPlan.Spec.Disabled {
|
||||
log.DebugContext(ctx, "skipped: spec.disabled is true", "skipped", true)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: concurrent renders
|
||||
results = make([]*v1alpha1.Result, 0, buildPlan.ResultCapacity())
|
||||
log.DebugContext(ctx, "allocated results slice", "cap", buildPlan.ResultCapacity())
|
||||
for _, component := range buildPlan.Spec.Components.Resources {
|
||||
if result, err := component.Render(ctx, path); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
for _, component := range buildPlan.Spec.Components.KubernetesObjectsList {
|
||||
if result, err := component.Render(ctx, path); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
for _, component := range buildPlan.Spec.Components.HelmChartList {
|
||||
if result, err := component.Render(ctx, path); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
for _, component := range buildPlan.Spec.Components.KustomizeBuildList {
|
||||
if result, err := component.Render(ctx, path); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
log.DebugContext(ctx, "returning results", "len", len(results))
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// findCueMod returns the root module location containing the cue.mod file or
|
||||
// directory or an error if the builder arguments do not share a common root
|
||||
// module.
|
||||
func (b *Builder) findCueMod() (dir holos.PathCueMod, err error) {
|
||||
for _, origPath := range b.cfg.args {
|
||||
absPath, err := filepath.Abs(origPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := holos.PathCueMod(absPath)
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(string(path), "cue.mod")); err == nil {
|
||||
if dir != "" && dir != path {
|
||||
return "", fmt.Errorf("multiple modules not supported: %v is not %v", dir, path)
|
||||
}
|
||||
dir = path
|
||||
break
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
parentPath := holos.PathCueMod(filepath.Dir(string(path)))
|
||||
if parentPath == path {
|
||||
return "", fmt.Errorf("no cue.mod from root to leaf: %v", origPath)
|
||||
}
|
||||
path = parentPath
|
||||
}
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
@@ -2,12 +2,13 @@ package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/holos-run/holos/internal/builder"
|
||||
"github.com/holos-run/holos/internal/cli/command"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/holos"
|
||||
"github.com/holos-run/holos/internal/internal/builder"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -20,10 +21,12 @@ func makeBuildRunFunc(cfg *holos.Config) command.RunFunc {
|
||||
return err
|
||||
}
|
||||
outs := make([]string, 0, len(results))
|
||||
for _, result := range results {
|
||||
if result.Skip {
|
||||
for idx, result := range results {
|
||||
if result == nil || result.Skip {
|
||||
slog.Debug("skip result", "idx", idx, "result", result)
|
||||
continue
|
||||
}
|
||||
slog.Debug("append result", "idx", idx, "result.kind", result.Kind)
|
||||
outs = append(outs, result.AccumulatedOutput())
|
||||
}
|
||||
out := strings.Join(outs, "---\n")
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/holos-run/holos/internal/builder"
|
||||
"github.com/holos-run/holos/internal/cli/command"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/holos"
|
||||
"github.com/holos-run/holos/internal/internal/builder"
|
||||
"github.com/holos-run/holos/internal/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -53,8 +54,9 @@ func New(cfg *holos.Config) *cobra.Command {
|
||||
// TODO: Avoid accidental over-writes if to holos component instances result in
|
||||
// the same file path. Write files into a blank temporary directory, error if a
|
||||
// file exists, then move the directory into place.
|
||||
for _, result := range results {
|
||||
if result.Skip {
|
||||
var result Result
|
||||
for _, result = range results {
|
||||
if result.Continue() {
|
||||
continue
|
||||
}
|
||||
// API Objects
|
||||
@@ -64,7 +66,7 @@ func New(cfg *holos.Config) *cobra.Command {
|
||||
}
|
||||
// Kustomization
|
||||
path = result.KustomizationFilename(cfg.WriteTo(), cfg.ClusterName())
|
||||
if err := result.Save(ctx, path, result.KsContent); err != nil {
|
||||
if err := result.Save(ctx, path, result.KustomizationContent()); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
log.InfoContext(ctx, "rendered "+result.Name(), "status", "ok", "action", "rendered", "name", result.Name())
|
||||
@@ -73,3 +75,13 @@ func New(cfg *holos.Config) *cobra.Command {
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
type Result interface {
|
||||
Continue() bool
|
||||
Name() string
|
||||
Filename(writeTo string, cluster string) string
|
||||
KustomizationFilename(writeTo string, cluster string) string
|
||||
Save(ctx context.Context, path string, content string) error
|
||||
AccumulatedOutput() string
|
||||
KustomizationContent() string
|
||||
}
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
// Package builder is responsible for building fully rendered kubernetes api
|
||||
// objects from various input directories. A directory may contain a platform
|
||||
// spec or a component spec.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/load"
|
||||
"github.com/holos-run/holos/api/v1alpha1"
|
||||
|
||||
"github.com/holos-run/holos"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
KubernetesObjects = v1alpha1.KubernetesObjectsKind
|
||||
// Helm is the value of the kind field of holos build output indicating helm
|
||||
// values and helm command information.
|
||||
Helm = v1alpha1.HelmChartKind
|
||||
// Skip is the value when the instance should be skipped
|
||||
Skip = "Skip"
|
||||
// KustomizeBuild is the value of the kind field of cue output indicating holos should process the component using kustomize build to render output.
|
||||
KustomizeBuild = v1alpha1.KustomizeBuildKind
|
||||
)
|
||||
|
||||
// An Option configures a Builder
|
||||
type Option func(*config)
|
||||
|
||||
type config struct {
|
||||
args []string
|
||||
cluster string
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
cfg config
|
||||
}
|
||||
|
||||
// New returns a new *Builder configured by opts Option.
|
||||
func New(opts ...Option) *Builder {
|
||||
var cfg config
|
||||
for _, f := range opts {
|
||||
f(&cfg)
|
||||
}
|
||||
b := &Builder{cfg: cfg}
|
||||
return b
|
||||
}
|
||||
|
||||
// Entrypoints configures the leaf directories Builder builds.
|
||||
func Entrypoints(args []string) Option {
|
||||
return func(cfg *config) { cfg.args = args }
|
||||
}
|
||||
|
||||
// Cluster configures the cluster name for the holos component instance.
|
||||
func Cluster(name string) Option {
|
||||
return func(cfg *config) { cfg.cluster = name }
|
||||
}
|
||||
|
||||
// Cluster returns the cluster name of the component instance being built.
|
||||
func (b *Builder) Cluster() string {
|
||||
return b.cfg.cluster
|
||||
}
|
||||
|
||||
// Instances returns the cue build instances being built.
|
||||
func (b *Builder) Instances(ctx context.Context) ([]*build.Instance, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
mod, err := b.findCueMod()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
dir := string(mod)
|
||||
|
||||
cfg := load.Config{Dir: dir}
|
||||
|
||||
// Make args relative to the module directory
|
||||
args := make([]string, len(b.cfg.args))
|
||||
for idx, path := range b.cfg.args {
|
||||
target, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not find absolute path: %w", err))
|
||||
}
|
||||
relPath, err := filepath.Rel(dir, target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("invalid argument, must be relative to cue.mod: %w", err))
|
||||
}
|
||||
relPath = "./" + relPath
|
||||
args[idx] = relPath
|
||||
equiv := fmt.Sprintf("cue export --out yaml -t cluster=%v %v", b.Cluster(), relPath)
|
||||
log.Debug("cue: equivalent command: " + equiv)
|
||||
}
|
||||
|
||||
// Refer to https://github.com/cue-lang/cue/blob/v0.7.0/cmd/cue/cmd/common.go#L429
|
||||
cfg.Tags = append(cfg.Tags, "cluster="+b.Cluster())
|
||||
log.DebugContext(ctx, fmt.Sprintf("cue: tags %v", cfg.Tags))
|
||||
|
||||
return load.Instances(args, &cfg), nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ctx context.Context) (results []*v1alpha1.Result, err error) {
|
||||
results = make([]*v1alpha1.Result, 0, len(b.cfg.args))
|
||||
cueCtx := cuecontext.New()
|
||||
logger.FromContext(ctx).DebugContext(ctx, "cue: building instances")
|
||||
instances, err := b.Instances(ctx)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
// Each CUE instance provides a BuildPlan
|
||||
for _, instance := range instances {
|
||||
var buildPlan v1alpha1.BuildPlan
|
||||
|
||||
log := logger.FromContext(ctx).With("dir", instance.Dir)
|
||||
if err := instance.Err; err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not load: %w", err))
|
||||
}
|
||||
log.DebugContext(ctx, "cue: building instance")
|
||||
value := cueCtx.BuildInstance(instance)
|
||||
if err := value.Err(); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not build %s: %w", instance.Dir, err))
|
||||
}
|
||||
log.DebugContext(ctx, "cue: validating instance")
|
||||
if err := value.Validate(); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not validate: %w", err))
|
||||
}
|
||||
|
||||
log.DebugContext(ctx, "cue: decoding holos build plan")
|
||||
// Hack to catch unknown fields https://github.com/holos-run/holos/issues/72
|
||||
jsonBytes, err := value.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not marshal cue instance %s: %w", instance.Dir, err))
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&buildPlan)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("invalid BuildPlan: %s: %w", instance.Dir, err))
|
||||
}
|
||||
|
||||
if err := buildPlan.Validate(); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not validate %s: %w", instance.Dir, err))
|
||||
}
|
||||
|
||||
if buildPlan.Spec.Disabled {
|
||||
log.DebugContext(ctx, "skipped: spec.disabled is true", "skipped", true)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: concurrent renders
|
||||
for _, component := range buildPlan.Spec.Components.Resources {
|
||||
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
for _, component := range buildPlan.Spec.Components.KubernetesObjectsList {
|
||||
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
for _, component := range buildPlan.Spec.Components.HelmChartList {
|
||||
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
for _, component := range buildPlan.Spec.Components.KustomizeBuildList {
|
||||
if result, err := component.Render(ctx, holos.InstancePath(instance.Dir)); err != nil {
|
||||
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
|
||||
} else {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// findCueMod returns the root module location containing the cue.mod file or
|
||||
// directory or an error if the builder arguments do not share a common root
|
||||
// module.
|
||||
func (b *Builder) findCueMod() (dir holos.PathCueMod, err error) {
|
||||
for _, origPath := range b.cfg.args {
|
||||
absPath, err := filepath.Abs(origPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := holos.PathCueMod(absPath)
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(string(path), "cue.mod")); err == nil {
|
||||
if dir != "" && dir != path {
|
||||
return "", fmt.Errorf("multiple modules not supported: %v is not %v", dir, path)
|
||||
}
|
||||
dir = path
|
||||
break
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
parentPath := holos.PathCueMod(filepath.Dir(string(path)))
|
||||
if parentPath == path {
|
||||
return "", fmt.Errorf("no cue.mod from root to leaf: %v", origPath)
|
||||
}
|
||||
path = parentPath
|
||||
}
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
4
internal/platforms/bare/components/cluster.cue
Normal file
4
internal/platforms/bare/components/cluster.cue
Normal file
@@ -0,0 +1,4 @@
|
||||
package holos
|
||||
|
||||
// #ClusterName is the --cluster-name flag value provided by the holos cli.
|
||||
#ClusterName: string @tag(cluster, type=string)
|
||||
@@ -1,26 +1,20 @@
|
||||
package holos
|
||||
|
||||
import ( "encoding/yaml"
|
||||
import "encoding/yaml"
|
||||
import v1 "github.com/holos-run/holos/api/v1alpha1"
|
||||
|
||||
)
|
||||
let PLATFORM = {message: "TODO: Load the platform from the API."}
|
||||
|
||||
// The platform configmap is a simple component that manages a configmap named
|
||||
// platform in the default namespace. The purpose is to exercise end to end
|
||||
// validation of platform configuration values provided by the holos web ui to
|
||||
// each cluster in the platform.
|
||||
platform: #Platform & {metadata: name: "bare"}
|
||||
let PLATFORM = platform
|
||||
|
||||
// spec represents the output provided to holos
|
||||
spec: components: KubernetesObjectsList: [
|
||||
#KubernetesObjects & {
|
||||
// Provide a BuildPlan to the holos cli to render k8s api objects.
|
||||
v1.#BuildPlan & {
|
||||
spec: components: resources: platformConfigmap: {
|
||||
metadata: name: "platform-configmap"
|
||||
apiObjectMap: OBJECTS.apiObjectMap
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// OBJECTS represents the kubernetes api objects to manage.
|
||||
let OBJECTS = #APIObjects & {
|
||||
let OBJECTS = v1.#APIObjects & {
|
||||
apiObjects: ConfigMap: platform: {
|
||||
metadata: {
|
||||
name: "platform"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package forms
|
||||
|
||||
import formsv1 "github.com/holos-run/forms/v1alpha1"
|
||||
import v1 "github.com/holos-run/holos/v1alpha1"
|
||||
|
||||
let Platform = formsv1.#Platform & {
|
||||
name: "bare"
|
||||
displayName: "Bare Platform"
|
||||
// Provides a concrete v1.#Form
|
||||
FormBuilder.Output
|
||||
|
||||
sections: org: {
|
||||
let FormBuilder = v1.#FormBuilder & {
|
||||
Sections: org: {
|
||||
displayName: "Organization"
|
||||
description: "Organization config values are used to derive more specific configuration values throughout the platform."
|
||||
|
||||
@@ -64,7 +64,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: cloud: {
|
||||
Sections: cloud: {
|
||||
displayName: "Cloud Providers"
|
||||
description: "Select the services that provide resources for the platform."
|
||||
|
||||
@@ -91,7 +91,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: aws: {
|
||||
Sections: aws: {
|
||||
displayName: "Amazon Web Services"
|
||||
description: "Provide the information necessary for Holos to manage AWS resources to provide the platform."
|
||||
|
||||
@@ -128,7 +128,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: gcp: {
|
||||
Sections: gcp: {
|
||||
displayName: "Google Cloud Platform"
|
||||
description: "Use this form to configure platform level GCP settings."
|
||||
|
||||
@@ -208,7 +208,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: cloudflare: {
|
||||
Sections: cloudflare: {
|
||||
displayName: "Cloudflare"
|
||||
description: "Cloudflare is primarily used for DNS automation."
|
||||
|
||||
@@ -228,7 +228,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: github: {
|
||||
Sections: github: {
|
||||
displayName: "GitHub"
|
||||
description: "GitHub is primarily used to host Git repositories and execute Actions workflows."
|
||||
|
||||
@@ -253,7 +253,7 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
|
||||
sections: backups: {
|
||||
Sections: backups: {
|
||||
displayName: "Backups"
|
||||
description: "Configure platform level data backup settings. Requires AWS."
|
||||
|
||||
@@ -278,76 +278,8 @@ let Platform = formsv1.#Platform & {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_sections: privacy: {
|
||||
displayName: "Data Privacy"
|
||||
description: "Configure data privacy aspects of the platform."
|
||||
|
||||
fieldConfigs: {
|
||||
country: {
|
||||
// https://formly.dev/docs/api/ui/material/select/
|
||||
type: "select"
|
||||
props: {
|
||||
label: "Select Planet"
|
||||
description: "Juridiction of applicable data privacy laws."
|
||||
options: [
|
||||
{value: "mercury", label: "Mercury"},
|
||||
{value: "venus", label: "Venus"},
|
||||
{value: "earth", label: "Earth"},
|
||||
{value: "mars", label: "Mars"},
|
||||
{value: "jupiter", label: "Jupiter"},
|
||||
{value: "saturn", label: "Saturn"},
|
||||
{value: "uranus", label: "Uranus"},
|
||||
{value: "neptune", label: "Neptune"},
|
||||
]
|
||||
}
|
||||
}
|
||||
regions: {
|
||||
// https://formly.dev/docs/api/ui/material/select/
|
||||
type: "select"
|
||||
props: {
|
||||
label: "Select Regions"
|
||||
description: "Select the regions this platform operates in."
|
||||
multiple: true
|
||||
selectAllOption: "Select All"
|
||||
options: [
|
||||
{value: "us-east-2", label: "Ohio"},
|
||||
{value: "us-west-2", label: "Oregon"},
|
||||
{value: "eu-west-1", label: "Ireland"},
|
||||
{value: "eu-west-2", label: "London", disabled: true},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_sections: terms: {
|
||||
displayName: "Terms and Conditions"
|
||||
description: "Example of a boolean checkbox."
|
||||
|
||||
fieldConfigs: {
|
||||
// platform.spec.config.user.sections.terms.fields.didAgree
|
||||
didAgree: {
|
||||
type: "checkbox"
|
||||
props: {
|
||||
label: "Accept terms"
|
||||
description: "In order to proceed, please accept terms"
|
||||
pattern: "true"
|
||||
required: true
|
||||
}
|
||||
validation: {
|
||||
messages: {
|
||||
pattern: "Please accept the terms"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the output form fields
|
||||
Platform.Form
|
||||
|
||||
let GCPRegions = [
|
||||
{value: "africa-south1", label: "africa-south1"},
|
||||
{value: "asia-east1", label: "asia-east1"},
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package holos
|
||||
|
||||
import (
|
||||
h "github.com/holos-run/holos/api/v1alpha1"
|
||||
"encoding/yaml"
|
||||
)
|
||||
|
||||
// CUE provides a #BuildPlan to the holos cli. Holos requires the output of CUE
|
||||
// to conform to the #BuildPlan schema.
|
||||
{} & h.#BuildPlan
|
||||
|
||||
// #HolosComponent defines struct fields common to all holos component types.
|
||||
#HolosComponent: {
|
||||
h.#HolosComponent
|
||||
metadata: name: string
|
||||
_NameLengthConstraint: len(metadata.name) & >=1
|
||||
...
|
||||
}
|
||||
|
||||
#KubernetesObjects: #HolosComponent & h.#KubernetesObjects
|
||||
|
||||
// #HolosTypeMeta is similar to kubernetes api TypeMeta, but for holos api
|
||||
// objects such as the Platform config resource.
|
||||
#HolosTypeMeta: {
|
||||
kind: string @go(Kind)
|
||||
apiVersion: string @go(APIVersion)
|
||||
}
|
||||
|
||||
// #HolosObjectMeta is similar to kubernetes api ObjectMeta, but for holos api
|
||||
// objects.
|
||||
#HolosObjectMeta: {
|
||||
name: string @go(Name)
|
||||
labels: {[string]: string} @go(Labels,map[string]string)
|
||||
annotations: {[string]: string} @go(Annotations,map[string]string)
|
||||
}
|
||||
|
||||
// #APIObjects defines the output format for kubernetes api objects. The holos
|
||||
// cli expects the yaml representation of each api object in the apiObjectMap
|
||||
// field.
|
||||
#APIObjects: {
|
||||
// apiObjects represents the un-marshalled form of each kubernetes api object
|
||||
// managed by a holos component.
|
||||
apiObjects: {
|
||||
[Kind=_]: {
|
||||
[string]: {
|
||||
kind: Kind
|
||||
...
|
||||
}
|
||||
}
|
||||
ConfigMap?: [Name=_]: #ConfigMap & {metadata: name: Name}
|
||||
}
|
||||
|
||||
// apiObjectMap holds the marshalled representation of apiObjects
|
||||
apiObjectMap: {
|
||||
for kind, v in apiObjects {
|
||||
"\(kind)": {
|
||||
for name, obj in v {
|
||||
"\(name)": yaml.Marshal(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package holos
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
_NamespaceObject: {
|
||||
metadata: name: string
|
||||
metadata: namespace: string
|
||||
metadata: labels: "app.holos.run/managed": "true"
|
||||
}
|
||||
|
||||
#ConfigMap: _NamespaceObject & corev1.#ConfigMap
|
||||
@@ -1,39 +0,0 @@
|
||||
package holos
|
||||
|
||||
// #Platform represents the user supplied platform configuration.
|
||||
#Platform: {
|
||||
#HolosTypeMeta
|
||||
kind: "Platform"
|
||||
apiVersion: "app.holos.run/v1alpha1"
|
||||
metadata: #HolosObjectMeta
|
||||
spec: #PlatformSpec
|
||||
holos: #Holos
|
||||
}
|
||||
|
||||
// #Holos represents the holos reserved field in the #Platform schema defined by the holos development team.
|
||||
#Holos: {
|
||||
// flags represents config values provided by holos command line flags.
|
||||
flags: {
|
||||
// cluster represents the holos render --cluster-name flag.
|
||||
cluster: string @tag(cluster, type=string)
|
||||
}
|
||||
}
|
||||
|
||||
// #PlatformSpec represents configuration values defined by the platform
|
||||
// designer. Config values are organized by section, then simple strings for
|
||||
// each section.
|
||||
#PlatformSpec: {
|
||||
config: [string]: _
|
||||
config: user: #UserDefinedConfig
|
||||
}
|
||||
|
||||
// #PlatformUserConfig represents configuration fields and values defined by the
|
||||
// user.
|
||||
#UserDefinedConfig: {
|
||||
sections: [string]: fields: [string]: _
|
||||
}
|
||||
|
||||
// #PlatformConfig represents the platform config data returned from the Holos API. Useful for cue vet.
|
||||
#PlatformConfig: {
|
||||
platform: spec: #PlatformSpec
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"platform": {
|
||||
"spec": {
|
||||
"config": {
|
||||
"user": {
|
||||
"sections": {
|
||||
"org": {
|
||||
"fields": {
|
||||
"contactEmail": "jeff@openinfrastructure.co",
|
||||
"displayName": "Open Infrastructure Services LLC",
|
||||
"domain": "ois.run",
|
||||
"name": "ois"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
internal/platforms/bare/readme.md
Normal file
20
internal/platforms/bare/readme.md
Normal file
@@ -0,0 +1,20 @@
|
||||
Bare Platform
|
||||
|
||||
| Folder | Description |
|
||||
| - | - |
|
||||
| forms | Contains Platform and Project form and model definitions |
|
||||
| platform | Contains the Platform resource that defines how to render the configuration for all Platform Components |
|
||||
| components | Contains BuildPlan resources which define how to render individual Platform Components |
|
||||
|
||||
## Forms
|
||||
|
||||
To populate the form, the platform must already be created in the Web UI:
|
||||
|
||||
```bash
|
||||
platformId="018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94"
|
||||
cue export ./forms/platform/ --out json \
|
||||
| jq '{platform_id: "'$platformId'", fields: .spec.fields}' \
|
||||
| grpcurl -H "x-oidc-id-token: $(holos token)" -d @ \
|
||||
app.dev.k2.holos.run:443 \
|
||||
holos.v1alpha1.PlatformService.PutForm
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
package holos
|
||||
@@ -0,0 +1,33 @@
|
||||
package v1alpha1
|
||||
|
||||
import "encoding/yaml"
|
||||
|
||||
import core "k8s.io/api/core/v1"
|
||||
|
||||
// #APIObjects defines the output format for kubernetes api objects. The holos
|
||||
// cli expects the yaml representation of each api object in the apiObjectMap
|
||||
// field.
|
||||
#APIObjects: {
|
||||
// apiObjects represents the un-marshalled form of each kubernetes api object
|
||||
// managed by a holos component.
|
||||
apiObjects: {
|
||||
[Kind=string]: {
|
||||
[string]: {
|
||||
kind: Kind
|
||||
...
|
||||
}
|
||||
}
|
||||
ConfigMap: [string]: core.#ConfigMap & {apiVersion: "v1"}
|
||||
}
|
||||
|
||||
// apiObjectMap holds the marshalled representation of apiObjects
|
||||
apiObjectMap: {
|
||||
for kind, v in apiObjects {
|
||||
"\(kind)": {
|
||||
for name, obj in v {
|
||||
"\(name)": yaml.Marshal(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package v1alpha1
|
||||
|
||||
#BuildPlan: {
|
||||
apiVersion: #APIVersion
|
||||
kind: #BuildPlanKind
|
||||
}
|
||||
@@ -1,35 +1,31 @@
|
||||
package v1alpha1
|
||||
|
||||
#Platform: {
|
||||
name: string // short dns label name
|
||||
displayName: string // Display name
|
||||
description: string // Plaform description
|
||||
// #Form represents a web app form to provide to the Holos API for display in
|
||||
// the web app. A form is implemented as an Formly FieldConfig array using
|
||||
// Angular Material form field components.
|
||||
#Form: {
|
||||
#TypeMeta
|
||||
apiVersion: #APIVersion
|
||||
kind: "Form"
|
||||
|
||||
sections: {[NAME=string]: #ConfigSection & {name: NAME}}
|
||||
spec: fields: [...#FieldConfig]
|
||||
}
|
||||
|
||||
Form: {
|
||||
let Name = name
|
||||
apiVersion: "forms.holos.run/v1alpha1"
|
||||
kind: "PlatformForm"
|
||||
metadata: name: Name
|
||||
spec: #PlatformFormSpec
|
||||
// #FormBuilder provides a concrete #Form via the Output field.
|
||||
#FormBuilder: {
|
||||
Name: string
|
||||
Sections: {[NAME=string]: #FormSection & {name: NAME}}
|
||||
|
||||
Output: #Form & {
|
||||
spec: fields: [for s in Sections {s.wrapper}]
|
||||
}
|
||||
|
||||
let Sections = sections
|
||||
|
||||
// Collapse all sections into one fields list.
|
||||
// Refer to https://formly.dev/docs/examples/other/nested-formly-forms
|
||||
Form: spec: fields: [for s in Sections {s.wrapper}]
|
||||
}
|
||||
|
||||
#PlatformFormSpec: {
|
||||
fields: [...#FieldConfig]
|
||||
}
|
||||
|
||||
// #ConfigSection represents a configuration section of the front end UI. For
|
||||
// example, Organization config values. The fields of the section map to form
|
||||
// input fields.
|
||||
#ConfigSection: {
|
||||
// #FormSection represents a configuration section of the front end UI. The
|
||||
// wrapper field provides a concrete #FieldConfig for the form section. The
|
||||
// fields of the section map to form input fields.
|
||||
// Refer to: to https://formly.dev/docs/examples/other/nested-formly-forms
|
||||
#FormSection: {
|
||||
name: string // e.g. "org"
|
||||
displayName: string // e.g. "Organization"
|
||||
description: string
|
||||
@@ -58,14 +54,7 @@ package v1alpha1
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE
|
||||
#ConfigSectionOutput: {
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
fieldConfigs: [...#FieldConfig]
|
||||
}
|
||||
|
||||
// #FieldConfig represents a Formly Field Config.
|
||||
// Refer to https://formly.dev/docs/api/core#formlyfieldconfig
|
||||
// Refer to https://formly.dev/docs/api/ui/material/select
|
||||
#FieldConfig: {
|
||||
@@ -0,0 +1,16 @@
|
||||
package v1alpha1
|
||||
|
||||
// #HolosComponent defines struct fields common to all holos platform
|
||||
// component types.
|
||||
#HolosComponent: {
|
||||
metadata: name: string
|
||||
_NameLengthConstraint: len(metadata.name) & >=1
|
||||
...
|
||||
}
|
||||
|
||||
// #KubernetesObjects is a Holos Component BuildPlan which has k8s api objects
|
||||
// embedded into the build plan itself.
|
||||
#KubernetesObjects: #HolosComponent & {
|
||||
apiVersion: #APIVersion
|
||||
kind: #KubernetesObjectsKind
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package v1alpha1
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
#ConfigMap: corev1.#ConfigMap & {
|
||||
apiVersion: "v1"
|
||||
kind: "ConfigMap"
|
||||
...
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package v1alpha1
|
||||
@@ -0,0 +1,20 @@
|
||||
package v1alpha1
|
||||
|
||||
// #Platform represents a platform to manage. Holos manages a platform by
|
||||
// rendering platform components and applying the configuration to clusters as
|
||||
// defined by the platform resource.
|
||||
#Platform: {
|
||||
#TypeMeta
|
||||
apiVersion: #APIVersion
|
||||
kind: "Platform"
|
||||
metadata: #ObjectMeta
|
||||
spec: {
|
||||
// model represents the user defined platform model, which is produced and
|
||||
// defined by the user supplied form.
|
||||
model: {...}
|
||||
|
||||
// components represents components to manage in the platform, organized by
|
||||
// the kind of cluster the rendered configuration applies to.
|
||||
components: {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package v1alpha1
|
||||
@@ -0,0 +1,16 @@
|
||||
package v1alpha1
|
||||
|
||||
// #TypeMeta is similar to kubernetes api TypeMeta, but for holos api
|
||||
// objects such as the Platform config resource.
|
||||
#TypeMeta: {
|
||||
kind: string @go(Kind)
|
||||
apiVersion: string @go(APIVersion)
|
||||
}
|
||||
|
||||
// #ObjectMeta is similar to kubernetes api ObjectMeta, but for holos api
|
||||
// objects.
|
||||
#ObjectMeta: {
|
||||
name: string @go(Name)
|
||||
labels: {[string]: string} @go(Labels,map[string]string)
|
||||
annotations: {[string]: string} @go(Annotations,map[string]string)
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
73
|
||||
74
|
||||
|
||||
@@ -1 +1 @@
|
||||
3
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user