diff --git a/.claude/settings.json b/.claude/settings.json index c8169bb7..6b27315f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,13 +1,18 @@ { "permissions": { "allow": [ - "Bash(cd:*)", + "Bash(make:*)", "Bash(holos:*)", "Bash(cue:*)", + "Bash(grep:*)", + "Bash(cd:*)", + "Bash(mkdir:*)", + "Bash(find:*)", + "Bash(echo:*)", + "Bash(head:*)", "Bash(git commit:*)", - "Bash(git add:*)", - "Bash(make:*)" + "Bash(git add:*)" ], "deny": [] } -} \ No newline at end of file +} diff --git a/.cspell.json b/.cspell.json index 41fe0548..9d0aaf1c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -308,6 +308,7 @@ "sysfs", "systemconnect", "tablewriter", + "taskset", "templatable", "testscript", "testutil", diff --git a/CLAUDE.md b/CLAUDE.md index 70baf935..e2578a48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,3 +113,4 @@ Component: #Helm & { - Test fixtures: `/internal/testutil/fixtures/` - Core schemas: `/api/core/` (Abstraction over low level data pipeline tasks) - Author schemas: `/api/author/` (User facing abstractions over core Schemas) +- Task planning documents are located in the `/tasks/` directory diff --git a/api/core/v1alpha6/types.go b/api/core/v1alpha6/types.go index df29c597..28e06076 100644 --- a/api/core/v1alpha6/types.go +++ b/api/core/v1alpha6/types.go @@ -382,6 +382,10 @@ type Command struct { IsStdoutOutput bool `json:"isStdoutOutput,omitempty" yaml:"isStdoutOutput,omitempty"` } +// NameLabel indicates a field name matching the name of the value. Usually the +// name or metadata.name field of the struct value. +type NameLabel string + // InternalLabel is an arbitrary unique identifier internal to holos itself. // The holos cli is expected to never write a InternalLabel value to rendered // output files, therefore use a InternalLabel when the identifier must be @@ -426,7 +430,7 @@ type Platform struct { // PlatformSpec represents the platform specification. type PlatformSpec struct { // Components represents a collection of holos components to manage. - Components []Component `json:"components" yaml:"components"` + Components map[NameLabel]Component `json:"components" yaml:"components" cue:"{[NAME=string]: name: NAME}"` } // Component represents the complete context necessary to produce a [BuildPlan] diff --git a/doc/md/api/core.md b/doc/md/api/core.md index cc6f87bf..ff000fa9 100644 --- a/doc/md/api/core.md +++ b/doc/md/api/core.md @@ -37,6 +37,7 @@ Package core contains schemas for a [Platform](<#Platform>) and [BuildPlan](<#Bu - [type Kustomization](<#Kustomization>) - [type Kustomize](<#Kustomize>) - [type Metadata](<#Metadata>) +- [type NameLabel](<#NameLabel>) - [type Platform](<#Platform>) - [type PlatformSpec](<#PlatformSpec>) - [type Repository](<#Repository>) @@ -478,6 +479,15 @@ type Metadata struct { } ``` + +## type NameLabel {#NameLabel} + +NameLabel indicates a field name matching the name of the value. Usually the name or metadata.name field of the struct value. + +```go +type NameLabel string +``` + ## type Platform {#Platform} @@ -511,7 +521,7 @@ PlatformSpec represents the platform specification. ```go type PlatformSpec struct { // Components represents a collection of holos components to manage. - Components []Component `json:"components" yaml:"components"` + Components map[NameLabel]Component `json:"components" yaml:"components" cue:"{[NAME=string]: name: NAME}"` } ``` diff --git a/internal/cli/show_test.go b/internal/cli/show_test.go index d6dda6d6..e164a98d 100644 --- a/internal/cli/show_test.go +++ b/internal/cli/show_test.go @@ -66,7 +66,7 @@ func TestShowAlpha6(t *testing.T) { t.Run("BuildPlans", func(t *testing.T) { t.Run("EmptyPlatform", func(t *testing.T) { - platformDir := filepath.Join(tempDir, "platform") + platformDir := filepath.Join(tempDir, "fixtures", "v1alpha6", "empty-platform") h := newHarness() err := h.Run(ctx, "buildplans", platformDir) require.NoError(t, err) diff --git a/internal/generate/platforms/cue.mod/gen/github.com/holos-run/holos/api/core/v1alpha6/types_go_gen.cue b/internal/generate/platforms/cue.mod/gen/github.com/holos-run/holos/api/core/v1alpha6/types_go_gen.cue index 66d899eb..cb518613 100644 --- a/internal/generate/platforms/cue.mod/gen/github.com/holos-run/holos/api/core/v1alpha6/types_go_gen.cue +++ b/internal/generate/platforms/cue.mod/gen/github.com/holos-run/holos/api/core/v1alpha6/types_go_gen.cue @@ -400,6 +400,10 @@ package core isStdoutOutput?: bool @go(IsStdoutOutput) } +// NameLabel indicates a field name matching the name of the value. Usually the +// name or metadata.name field of the struct value. +#NameLabel: string + // InternalLabel is an arbitrary unique identifier internal to holos itself. // The holos cli is expected to never write a InternalLabel value to rendered // output files, therefore use a InternalLabel when the identifier must be @@ -448,7 +452,7 @@ package core // PlatformSpec represents the platform specification. #PlatformSpec: { // Components represents a collection of holos components to manage. - components: [...#Component] @go(Components,[]Component) + components: {[string]: #Component} & {[NAME=string]: name: NAME} @go(Components,map[NameLabel]Component) } // Component represents the complete context necessary to produce a [BuildPlan] diff --git a/internal/generate/platforms/cue.mod/pkg/github.com/holos-run/holos/api/author/v1alpha6/definitions.cue b/internal/generate/platforms/cue.mod/pkg/github.com/holos-run/holos/api/author/v1alpha6/definitions.cue index e876d83b..52cc5d08 100644 --- a/internal/generate/platforms/cue.mod/pkg/github.com/holos-run/holos/api/author/v1alpha6/definitions.cue +++ b/internal/generate/platforms/cue.mod/pkg/github.com/holos-run/holos/api/author/v1alpha6/definitions.cue @@ -10,7 +10,7 @@ import ( components: _ resource: { metadata: "name": name - spec: "components": [for x in components {x}] + spec: "components": components } } diff --git a/internal/generate/platforms/v1alpha6-examples/.gitignore b/internal/generate/platforms/v1alpha6-examples/.gitignore new file mode 100644 index 00000000..c9bff312 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +vendor/ +node_modules/ +tmp/ diff --git a/internal/generate/platforms/v1alpha6-examples/components/taskset/component.cue b/internal/generate/platforms/v1alpha6-examples/components/taskset/component.cue new file mode 100644 index 00000000..770c8c3e --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/components/taskset/component.cue @@ -0,0 +1,30 @@ +package holos + +// This component produces a TaskSet at the top level holos field +// The TaskSet structure is to be defined +holos: { + metadata: name: "taskset-example" + + // TaskSet structure placeholder - to be defined + // This will represent the new v1alpha6 TaskSet that replaces BuildPlan + taskSet: { + // Example structure for discussion + tasks: { + // Tasks as a struct instead of list for better composition + generateManifests: { + type: "generator" + // Additional fields to be defined + } + transformManifests: { + type: "transformer" + dependsOn: ["generateManifests"] + // Additional fields to be defined + } + validateManifests: { + type: "validator" + dependsOn: ["transformManifests"] + // Additional fields to be defined + } + } + } +} \ No newline at end of file diff --git a/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.cue b/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.cue new file mode 100644 index 00000000..629022e8 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.cue @@ -0,0 +1,6 @@ +@extern(embed) +package holos + +import "github.com/holos-run/holos/api/core/v1alpha6:core" + +holos: core.#Component @embed(file=typemeta.yaml) \ No newline at end of file diff --git a/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.yaml b/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.yaml new file mode 100644 index 00000000..f3b09110 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/components/taskset/typemeta.yaml @@ -0,0 +1,2 @@ +kind: Component +apiVersion: v1alpha6 \ No newline at end of file diff --git a/internal/generate/platforms/v1alpha6-examples/platform/platform.gen.cue b/internal/generate/platforms/v1alpha6-examples/platform/platform.gen.cue new file mode 100644 index 00000000..0ed09571 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/platform/platform.gen.cue @@ -0,0 +1,17 @@ +package holos + +platform: spec: components: { + TaskSet1: { + name: "TaskSet1" + path: "components/taskset" + // Parameters to pass into the component. + parameters: index: "1" + // In v1alpha5 "component" was ambiguous. We disambiguate in v1alpha6 by + // naming the output of a platform component an "instance" + labels: "app.holos.run/component.instance": name + // The component the instance is derived from. + labels: "app.holos.run/component.path": path + } +} + +holos: platform diff --git a/internal/generate/platforms/v1alpha6-examples/platform/typemeta.cue b/internal/generate/platforms/v1alpha6-examples/platform/typemeta.cue new file mode 100644 index 00000000..8f0b117c --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/platform/typemeta.cue @@ -0,0 +1,6 @@ +@extern(embed) +package holos + +import "github.com/holos-run/holos/api/core/v1alpha6:core" + +holos: core.#Platform @embed(file=typemeta.yaml) diff --git a/internal/generate/platforms/v1alpha6-examples/platform/typemeta.yaml b/internal/generate/platforms/v1alpha6-examples/platform/typemeta.yaml new file mode 100644 index 00000000..3e7d97c4 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/platform/typemeta.yaml @@ -0,0 +1,2 @@ +kind: Platform +apiVersion: v1alpha6 diff --git a/internal/generate/platforms/v1alpha6-examples/resources.cue b/internal/generate/platforms/v1alpha6-examples/resources.cue new file mode 100644 index 00000000..2e78979b --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/resources.cue @@ -0,0 +1,49 @@ +package holos + +import ( + corev1 "k8s.io/api/core/v1" + appsv1 "k8s.io/api/apps/v1" + rbacv1 "k8s.io/api/rbac/v1" + batchv1 "k8s.io/api/batch/v1" + + ci "cert-manager.io/clusterissuer/v1" + rgv1 "gateway.networking.k8s.io/referencegrant/v1beta1" + certv1 "cert-manager.io/certificate/v1" + hrv1 "gateway.networking.k8s.io/httproute/v1" + gwv1 "gateway.networking.k8s.io/gateway/v1" + ap "argoproj.io/appproject/v1alpha1" + es "external-secrets.io/externalsecret/v1beta1" + ss "external-secrets.io/secretstore/v1beta1" +) + +#Resources: { + [Kind=string]: [InternalLabel=string]: { + kind: Kind + metadata: name: string | *InternalLabel + } + + AppProject?: [_]: ap.#AppProject + Certificate?: [_]: certv1.#Certificate + ClusterIssuer?: [_]: ci.#ClusterIssuer + ClusterRole?: [_]: rbacv1.#ClusterRole + ClusterRoleBinding?: [_]: rbacv1.#ClusterRoleBinding + ConfigMap?: [_]: corev1.#ConfigMap + CronJob?: [_]: batchv1.#CronJob + Deployment?: [_]: appsv1.#Deployment + ExternalSecret?: [_]: es.#ExternalSecret + HTTPRoute?: [_]: hrv1.#HTTPRoute + Job?: [_]: batchv1.#Job + Namespace?: [_]: corev1.#Namespace + ReferenceGrant?: [_]: rgv1.#ReferenceGrant + Role?: [_]: rbacv1.#Role + RoleBinding?: [_]: rbacv1.#RoleBinding + Secret?: [_]: corev1.#Secret + SecretStore?: [_]: ss.#SecretStore + Service?: [_]: corev1.#Service + ServiceAccount?: [_]: corev1.#ServiceAccount + StatefulSet?: [_]: appsv1.#StatefulSet + + Gateway?: [_]: gwv1.#Gateway & { + spec: gatewayClassName: string | *"istio" + } +} diff --git a/internal/generate/platforms/v1alpha6-examples/schema.cue b/internal/generate/platforms/v1alpha6-examples/schema.cue new file mode 100644 index 00000000..ec95f222 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/schema.cue @@ -0,0 +1,37 @@ +package holos + +import "github.com/holos-run/holos/api/author/v1alpha6:author" + +#ComponentConfig: author.#ComponentConfig & { + Name: _Tags.component.name + Path: _Tags.component.path + Resources: #Resources + + // labels is an optional field, guard references to it. + if _Tags.component.labels != _|_ { + Labels: _Tags.component.labels + } + + // annotations is an optional field, guard references to it. + if _Tags.component.annotations != _|_ { + Annotations: _Tags.component.annotations + } +} + +// https://holos.run/docs/api/author/v1alpha6/#Kubernetes +#Kubernetes: close({ + #ComponentConfig + author.#Kubernetes +}) + +// https://holos.run/docs/api/author/v1alpha6/#Kustomize +#Kustomize: close({ + #ComponentConfig + author.#Kustomize +}) + +// https://holos.run/docs/api/author/v1alpha6/#Helm +#Helm: close({ + #ComponentConfig + author.#Helm +}) diff --git a/internal/generate/platforms/v1alpha6-examples/tags.cue b/internal/generate/platforms/v1alpha6-examples/tags.cue new file mode 100644 index 00000000..4023a481 --- /dev/null +++ b/internal/generate/platforms/v1alpha6-examples/tags.cue @@ -0,0 +1,34 @@ +package holos + +import ( + "encoding/json" + + "github.com/holos-run/holos/api/core/v1alpha6:core" +) + +// Note: tags should have a reasonable default value for cue export. +_Tags: { + // Standardized parameters + component: core.#Component & { + name: string | *"no-name" @tag(holos_component_name, type=string) + path: string | *"no-path" @tag(holos_component_path, type=string) + + _labels_json: string | *"" @tag(holos_component_labels, type=string) + _labels: {} + if _labels_json != "" { + _labels: json.Unmarshal(_labels_json) + } + for k, v in _labels { + labels: (k): v + } + + _annotations_json: string | *"" @tag(holos_component_annotations, type=string) + _annotations: {} + if _annotations_json != "" { + _annotations: json.Unmarshal(_annotations_json) + } + for k, v in _annotations { + annotations: (k): v + } + } +} diff --git a/internal/testutil/fixtures/v1alpha6/empty-platform/platform.cue b/internal/testutil/fixtures/v1alpha6/empty-platform/platform.cue new file mode 100644 index 00000000..379186fd --- /dev/null +++ b/internal/testutil/fixtures/v1alpha6/empty-platform/platform.cue @@ -0,0 +1,14 @@ +package holos + +import "github.com/holos-run/holos/api/author/v1alpha6:author" + +// holos represents the field holos render platform evaluates, the resource +// field of the author.#Platform definition constructed from a components +// struct. +holos: platform.resource + +platform: author.#Platform & { + components: { + // Empty platform with no components + } +} \ No newline at end of file diff --git a/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.cue b/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.cue new file mode 100644 index 00000000..a034d60b --- /dev/null +++ b/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.cue @@ -0,0 +1,6 @@ +@extern(embed) +package holos + +import "github.com/holos-run/holos/api/core/v1alpha6:core" + +holos: core.#Platform @embed(file=typemeta.yaml) \ No newline at end of file diff --git a/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.yaml b/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.yaml new file mode 100644 index 00000000..1b48f5ac --- /dev/null +++ b/internal/testutil/fixtures/v1alpha6/empty-platform/typemeta.yaml @@ -0,0 +1,2 @@ +kind: Platform +apiVersion: v1alpha6 \ No newline at end of file diff --git a/internal/testutil/fixtures/v1alpha6/platform1/platform.cue b/internal/testutil/fixtures/v1alpha6/platform1/platform.cue index 6ca1768d..c1e54347 100644 --- a/internal/testutil/fixtures/v1alpha6/platform1/platform.cue +++ b/internal/testutil/fixtures/v1alpha6/platform1/platform.cue @@ -7,8 +7,8 @@ holos: { "name": "default" } "spec": { - "components": [ - { + "components": { + "slice": { "annotations": { "app.holos.run/description": "slice command transformer" } @@ -20,7 +20,7 @@ holos: { "outputBaseDir": "outputBaseDir" } "path": "fixtures/v1alpha6/components/slice" - }, - ] + } + } } }