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"
- },
- ]
+ }
+ }
}
}