Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,22 @@ these are the corners where drift would start if it starts anywhere.
column (`TestPrunerGuards`), child guard independent of source. **Unblocks
K2** (gitops mode needs reliable `source=git`).

- [ ] **K4 — Generic Platform + deployment methods as providers.** The intent
- [~] **K4 — Generic Platform + deployment methods as providers.** The intent
for `Platform` is NOT a curated grab-bag of preferred charts but **generic
support**, Terraform-provider style: (a) make Platform components
data-declared (any chart via `{name, chart, namespace, values}`), with
traefik/cloudflared/nvidia/nfs/… as optional named **presets** or examples
— not baked-in opinion; (b) treat the deployment **method** itself as a
provider — Helm/Manifest/Argo are already kinds; add new methods
(Kustomize, Flux, …) as new kinds/providers rather than bespoke code.
openctl already has the provider bones (it's TF-like for infra); extend
the same shape to workload deployment so breadth comes from providers, not
special cases.
support**, Terraform-provider style.
- [x] **(a) Data-declared components — SHIPPED.** Platform now installs
ANY chart with no code change via a generic `components: {<name>:
{chart: {repo, name, version?}, namespace?, values?, wait?, enabled?}}`
map (`enabled` defaults true; a generic name overrides a same-named
preset). `traefik/cloudflared/argocd/nvidia/nfs/longhorn` are now
explicitly **presets** — default chart coords layered on the generic
path, not baked-in opinion. `enabledComponents` merges presets +
generic deterministically; `#Platform.components?` schema. Tests: generic
install, enabled:false + missing-chart skip, preset override, ordering.
- [ ] **(b) Deployment methods as providers.** Treat the deployment
**method** itself as a provider — Helm/Manifest/Argo are already kinds;
add new methods (Kustomize, Flux, …) as new kinds/providers rather than
bespoke code, so breadth comes from providers, not special cases.

- [ ] **K5 — Re-baseline the scope wedge (`direction.md`).** The doc originally
vetoed workloads/PaaS ("stop at cluster-Ready"); the deployment model +
Expand Down
125 changes: 96 additions & 29 deletions plugins/k8s/internal/provider/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ type component struct {
defaultNS string
}

// platformComponents is the curated, opinionated set. Traefik is the ingress
// (ingress-nginx is in maintenance; no Cilium-CNI dependency); cloudflared wires
// a Cloudflare Tunnel to in-cluster services.
// platformComponents is a set of named PRESETS — convenience defaults for
// common charts, NOT an exclusive list. A preset only fills in default chart
// coordinates so you enable it with `<name>: {enabled: true}`; the generic
// `components` map (see enabledComponents) installs ANY chart without a code
// change, which is the primary, Terraform-provider-style path. Presets are
// examples/shortcuts layered on top of that generic support, not baked-in
// opinion. Traefik is the ingress (ingress-nginx is in maintenance; no
// Cilium-CNI dependency); cloudflared wires a Cloudflare Tunnel to services.
var platformComponents = []component{
{"traefik", "https://traefik.github.io/charts", "traefik", "traefik"},
{"cloudflared", "https://cloudflare.github.io/helm-charts", "cloudflare-tunnel-remote", "cloudflared"},
Expand Down Expand Up @@ -80,43 +85,105 @@ type componentRelease struct {
values map[string]any
}

// buildComponentRelease resolves one component from its raw spec block, layering
// per-component overrides (namespace/chart/values/wait) over the supplied
// defaults. For a named preset, def carries the default chart coords; for a
// generic component, def is zero and the chart must be fully declared.
func buildComponentRelease(name string, raw map[string]any, def component) componentRelease {
ns := specString(raw, "namespace")
if ns == "" {
ns = def.defaultNS
}
if ns == "" {
ns = name // generic component with no namespace → its own name
}
chart := chartSpec{Repo: def.defaultRepo, Name: def.defaultChart}
if cr, ok := raw["chart"].(map[string]any); ok {
if v := specString(cr, "repo"); v != "" {
chart.Repo = v
}
if v := specString(cr, "name"); v != "" {
chart.Name = v
}
chart.Version = specString(cr, "version")
}
values, _ := raw["values"].(map[string]any)
return componentRelease{
comp: component{name: name, defaultRepo: chart.Repo, defaultChart: chart.Name, defaultNS: ns},
chart: chart,
opts: releaseOpts{
releaseName: name,
namespace: ns,
createNamespace: true,
values: values,
wait: specBool(raw, "wait"),
timeout: 5 * time.Minute,
},
values: values,
}
}

// enabledComponents parses the Platform spec into the set of enabled component
// releases, applying per-component overrides over the curated defaults.
// releases. Two sources feed it, both opt-in:
//
// - Named PRESETS (platformComponents): enable with `<name>: {enabled: true}`;
// the preset supplies default chart coordinates, which the block may still
// override (chart/namespace/values/wait).
// - The generic `components` map: `components: {<name>: {chart: {repo, name,
// version?}, namespace?, values?, wait?, enabled?}}` installs ANY chart with
// no code change (the Terraform-provider-style path). `enabled` defaults
// true for a listed generic component (set false to disable without
// removing). A generic entry sharing a preset's name overrides it.
//
// Order is deterministic: presets in declaration order, then generic components
// sorted by name; a generic override keeps the earlier slot.
func enabledComponents(spec map[string]any) []componentRelease {
var out []componentRelease
byName := map[string]componentRelease{}
var order []string
put := func(name string, cr componentRelease) {
if _, seen := byName[name]; !seen {
order = append(order, name)
}
byName[name] = cr
}

for _, c := range platformComponents {
raw, _ := spec[c.name].(map[string]any)
if raw == nil || !specBool(raw, "enabled") {
continue
}
ns := specString(raw, "namespace")
if ns == "" {
ns = c.defaultNS
put(c.name, buildComponentRelease(c.name, raw, c))
}

if generic, ok := spec["components"].(map[string]any); ok {
names := make([]string, 0, len(generic))
for name := range generic {
names = append(names, name)
}
chart := chartSpec{Repo: c.defaultRepo, Name: c.defaultChart}
if cr, ok := raw["chart"].(map[string]any); ok {
if v := specString(cr, "repo"); v != "" {
chart.Repo = v
sort.Strings(names)
for _, name := range names {
raw, _ := generic[name].(map[string]any)
if raw == nil {
continue
}
if v := specString(cr, "name"); v != "" {
chart.Name = v
// enabled defaults true for a listed generic component; only an
// explicit false disables it.
if v, ok := raw["enabled"].(bool); ok && !v {
continue
}
chart.Version = specString(cr, "version")
cr := buildComponentRelease(name, raw, component{})
// Can't install without chart coordinates; skip (the CUE schema
// requires chart.repo + chart.name, so this is defensive).
if cr.chart.Repo == "" || cr.chart.Name == "" {
continue
}
put(name, cr)
}
values, _ := raw["values"].(map[string]any)
out = append(out, componentRelease{
comp: c,
chart: chart,
opts: releaseOpts{
releaseName: c.name,
namespace: ns,
createNamespace: true,
values: values,
wait: specBool(raw, "wait"),
timeout: 5 * time.Minute,
},
values: values,
})
}

out := make([]componentRelease, 0, len(order))
for _, n := range order {
out = append(out, byName[n])
}
return out
}
Expand Down
84 changes: 84 additions & 0 deletions plugins/k8s/internal/provider/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,90 @@ func TestEnabledComponents_NvidiaDevicePlugin(t *testing.T) {
}
}

// TestEnabledComponents_Generic covers the K4(a) generic path: an arbitrary
// chart declared under `components` installs with no preset, defaults enabled
// to true, defaults its namespace to its name, and threads chart+values.
func TestEnabledComponents_Generic(t *testing.T) {
comps := enabledComponents(map[string]any{
"components": map[string]any{
"prometheus": map[string]any{
"chart": map[string]any{"repo": "https://prometheus-community.github.io/helm-charts", "name": "kube-prometheus-stack", "version": "65.0.0"},
"values": map[string]any{"grafana": map[string]any{"enabled": false}},
},
},
})
if len(comps) != 1 {
t.Fatalf("enabled = %d, want 1 (generic prometheus)", len(comps))
}
c := comps[0]
if c.comp.name != "prometheus" || c.opts.releaseName != "prometheus" {
t.Errorf("name/release = %s/%s", c.comp.name, c.opts.releaseName)
}
if c.chart.Repo != "https://prometheus-community.github.io/helm-charts" || c.chart.Name != "kube-prometheus-stack" || c.chart.Version != "65.0.0" {
t.Errorf("chart wrong: %+v", c.chart)
}
if c.opts.namespace != "prometheus" { // defaults to the release name
t.Errorf("namespace = %q, want prometheus (defaulted to name)", c.opts.namespace)
}
g, _ := c.values["grafana"].(map[string]any)
if g["enabled"] != false {
t.Errorf("values not threaded: %+v", c.values)
}
}

// TestEnabledComponents_GenericDisabledAndInvalid: an explicit enabled:false
// generic component is skipped, and one without chart coordinates is skipped
// (defensive — the schema requires chart.repo+name).
func TestEnabledComponents_GenericDisabledAndInvalid(t *testing.T) {
comps := enabledComponents(map[string]any{
"components": map[string]any{
"off": map[string]any{"enabled": false, "chart": map[string]any{"repo": "r", "name": "n"}},
"nochart": map[string]any{"namespace": "x"},
"malformed": map[string]any{"chart": map[string]any{"repo": "r"}}, // no name
},
})
if len(comps) != 0 {
t.Fatalf("enabled = %d, want 0 (all skipped)", len(comps))
}
}

// TestEnabledComponents_GenericOverridesPreset: a generic component sharing a
// preset's name overrides that preset's chart, keeping a single release.
func TestEnabledComponents_GenericOverridesPreset(t *testing.T) {
comps := enabledComponents(map[string]any{
"traefik": map[string]any{"enabled": true},
"components": map[string]any{
"traefik": map[string]any{"chart": map[string]any{"repo": "https://example.test/charts", "name": "my-traefik"}},
},
})
if len(comps) != 1 {
t.Fatalf("enabled = %d, want 1 (deduped)", len(comps))
}
if comps[0].chart.Name != "my-traefik" || comps[0].chart.Repo != "https://example.test/charts" {
t.Errorf("generic should override preset chart, got %+v", comps[0].chart)
}
}

// TestEnabledComponents_PresetAndGenericOrder: presets come first in
// declaration order, then generic components sorted by name.
func TestEnabledComponents_PresetAndGenericOrder(t *testing.T) {
comps := enabledComponents(map[string]any{
"traefik": map[string]any{"enabled": true},
"components": map[string]any{
"zzz": map[string]any{"chart": map[string]any{"repo": "r", "name": "z"}},
"aaa": map[string]any{"chart": map[string]any{"repo": "r", "name": "a"}},
},
})
got := []string{}
for _, c := range comps {
got = append(got, c.comp.name)
}
want := []string{"traefik", "aaa", "zzz"}
if len(got) != 3 || got[0] != want[0] || got[1] != want[1] || got[2] != want[2] {
t.Errorf("order = %v, want %v", got, want)
}
}

func TestPlanPlatform(t *testing.T) {
p := New()
m := platformResource(map[string]any{
Expand Down
17 changes: 17 additions & 0 deletions plugins/k8s/internal/provider/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ const platformSchema = `
// a k3s worker pool's nodePrep.packages, or Longhorn's iscsi-installer
// DaemonSet as a fallback.
longhorn?: {...}
// components installs ANY Helm chart, no code change required — the
// generic, Terraform-provider-style path (the named fields above are
// just presets layered on this). Key is the release name. enabled
// defaults true (listing a component opts in); set false to disable it
// without removing the block. A generic component sharing a preset's
// name overrides that preset.
components?: {[string]: {
enabled?: bool | *true
chart: {
repo: string
name: string
version?: string
}
namespace?: string
wait?: bool
values?: {...}
}}
}
...
}
Expand Down
Loading