Helm Catalog Test Findings

A repository document, rendered for the site. View source markdown.

What this command does. cub installer is a released, open-source plugin for the cub CLI. cub installer setup pulls a catalog package and writes its Kubernetes files locally. It does not apply those files to a cluster; use kubectl, Argo CD, or Flux for delivery. The generated scripts stop before doing any work when the plugin or kustomize is missing.

New to cub? Install the cub CLI first. You can pull and render public catalog packages without an account. Commands that save or change ConfigHub data require you to sign in.

Generated at: 2026-07-30T12:38:02.000Z UTC · source: committed helm-expt evidence for this rendered repository document.

_Findings (F1-F4) under the helm-expt test map._

Findings from earlier live runs of the helm-expt catalog test lanes. The project claim is that the catalog path should match Helm where it claims parity and add reviewable ConfigHub controls. Silent wrong output is therefore a release blocker for any affected path.


F1 - cub installer setup --namespace X silently splits the install (HIGH)

Severity: HIGH - silent wrong result; worse than vanilla Helm. Must fix before newcomers touch the catalog.

Symptom. cub installer setup --pull <pkg> --base <base> --namespace X where X ≠ the package's frozen namespace produces an incoherent render:

Applied as-is, the workloads target a namespace nothing in the bundle creates. First observed: --namespace nginx-wave1 → Namespace nginx-wave1, but Deployment/Service/NetworkPolicy/PDB/ServiceAccount all in nginx.

Root cause (verified, not inferred). Three facts together:

  1. The installer generates the Namespace object from --namespace and exposes it to chain templates as {{ .Namespace }} (installer setup --help).
  2. The base's bases/<base>/upstream.yaml has the namespace baked in as a literal (namespace: "nginx") - frozen at recipe-generation time from the upstream Helm render. It is not {{ .Namespace }}.
  3. The base's bases/<base>/kustomization.yaml is just resources: [upstream.yaml] - it has no kustomize namespace: transformer, so nothing rewrites the frozen literals.

So the install-namespace knob reaches the generated Namespace object but never reaches the frozen workload resources. The package is internally inconsistent w.r.t. namespace parameterization.

Scope: systemic, not nginx-only. Confirmed the same shape in bitnami/redis/25.5.3/bases/default (14 resources hardcode namespace: "redis", no kustomize namespace: transformer). This is how the recipe-generation pipeline froze every package, so it likely affects the whole catalog.

Why it matters for the thesis. helm install -n my-ns namespaces everything correctly. cub installer setup --namespace my-ns currently does not (for any ns ≠ frozen) and fails silently - that is strictly worse than Helm. A newcomer choosing their own namespace would get a broken install with no error.

Fix - CORRECTED after reading the installer source (2026-06-01). My first draft put the fix on the installer; that was wrong. The installer is not the primary culprit:

So:

Open: exactly what the installer does when --namespace is set on a hardcoded/no-transformer package (attempts a transform and no-ops, or never attempts one) needs a closer read of compose.go; the behavioral result (split render) is confirmed either way.

RESOLVED 2026-06-01 (18/20). Root cause confirmed 100% from the installer source: internal/render/compose.go wires the transform pass when --namespace is set, but with zero author groups it only runs ensureNamespace (creates the Namespace object) - it does NOT rewrite resource namespaces. Real re-namespacing requires the package to include the set-namespace transformer in its chain (the installer's own examples/hello-app/installer.yaml does this; so does the catalog's istio/istiod via {{ .Namespace }} templating).

Fix: added the canonical spec.transformers: [set-namespace "{{ .Namespace }}"] block to each package's installer.yaml via the reusable, idempotent scripts/helm-expt-fix-namespace-transformer (PyYAML-validated). Applied to all 20 proof packages. Verified: 18/20 now render coherently with an arbitrary namespace (e.g. nginx --namespace nginx-fixtest → all resources + Namespace object in nginx-fixtest); previously 0 honored a non-frozen namespace.

Status: primary fix in place for 18/20; see F1b for the 2 that need more. The helm-expt installer.yaml edits are made in the helm-expt working tree (a separate repo); committing them there is a deliberate cross-repo step.


F1b - set-namespace doesn't rewrite namespace refs embedded in spec (MEDIUM)

After the F1 fix, 2 of 20 still render split with an arbitrary namespace: kube-prometheus-stack (keeps monitoring) and consul (keeps consul). set-namespace sets metadata.namespace on namespaced resources, but does not rewrite namespace strings buried in spec fields - RBAC subjects[].namespace, webhook clientConfig.service.namespace, ConfigMap data, etc. Complex charts (RBAC/webhooks/operators) therefore retain their canonical namespace in those refs.

Implications:


F2 - default base ships bitnami/nginx:latest (MEDIUM, observe)

The running image is registry-1.docker.io/bitnami/nginx:latest. A :latest tag is non-deterministic (defeats pinned, reproducible delivery - the whole point of OCI-pinned GitOps). This is the upstream chart's default, carried through unchanged. A stress-test/scan should flag it; the catalog may want to pin digests in the default base. Not a Wave 1 blocker; recorded for the scan/Day-2 waves.


F3 - secret-dependent charts deliver "green" but can't start (HIGH)

Severity: HIGH - silent: Argo and cub-scout report Synced while the workload sits in CreateContainerConfigError. ~6/20 charts affected.

Symptom. Charts whose default base renders a Secret (the static-passwords bases + redis default) deploy via OCI→Argo to a pod stuck in CreateContainerConfigError. Verified live on grafana: pod needs secret 'grafana' (admin-user/admin-password, Optional: false); no such secret in the namespace; deploy 0/1.

Root cause (confirmed). cub installer upload never uploads rendered Secrets (installer upload --help: "Files in out/secrets/ are never uploaded… apply them out-of-band"). The Secret is rendered to out/secrets/ but is not a ConfigHub unit, not in the OCI bundle; Argo never creates it. The local-kind e2e applies out/secrets by hand; the OCI→Argo path has no equivalent step. This is the correct security choice (no plaintext in the artifact) with a missing replacement (a GitOps secret-delivery mechanism).

Scope. ~6/20 proof charts render a Secret in their default base: redis, postgresql, mysql, mongodb, rabbitmq, grafana. (nginx http-clusterip needs no Secret - why Wave 1 passed.)

Verified via cub-scout (read-only). cub-scout explain deployment/grafana confirmed the chain (Owner ArgoCD; Source oci://oci.hub.confighub.com…; ConfigHub OCI → Application/grafana → Deployment/grafana; Health Synced). cub-scout doctor reported Error: 0. So the chain delivered correctly and the failure is silent at the governance/diagnosis layer - the strongest argument for a required-secret validation gate.

Side-finding: scripts/cub-check-diagnosis-contradiction watches for ImagePullBackOff/CrashLoopBackOff/Error but not CreateContainerConfigError, so it didn't fire here - a one-line coverage gap to fix.

Fix / design. Not a per-package typo - a delivery-architecture capability, designed as a tiered secrets ladder (anonymous secret-free → connected "safe dev" out-of-band → paid ESO+Vault+rotation) with a universal required-secret validation gate as the first buildable.

Status: found and verified live. Secret-needing charts must be tested via the existing-secret / ESO path, not the password-generating default.

F4 - F1's installer.yaml change (PR #95) breaks the proof/compare suite (MEDIUM-HIGH)

Severity: MEDIUM-HIGH - the catalog's own proof/compare suite is red for the 20 changed charts until proof artifacts are regenerated.

Symptom. After helm-expt PR #95 (the F1 set-namespace fix) merged, npm run <chart>:compare and the proof suite fail for the 20 charts: Error: source file SHA mismatch for installer.yaml (scripts/lib/proof-common.mjs).

Root cause. The proof scripts verify a recorded SHA of each package's installer.yaml. PR #95 changed installer.yaml without regenerating the proof artifacts that record that SHA → mismatch. (redis's dedicated comparator skips the SHA check, so it still passed - masking the breakage in a quick look.)

Confirmed - not a render diff. Reverting the 20 installer.yaml to pre-#95 (f3efc50~1) and re-running the full compare sweep → 20/20 PASS (helm-equivalent; redis 14/14 + 13/13 semantic object matches). So the underlying installer↔Helm equivalence is intact; only the recorded SHA is stale.

Fix. Regenerate the proof artifacts for the 20 changed packages (recorded installer.yaml SHA + dependent rendered/receipt artifacts) and commit alongside the change. Rule: any installer.yaml edit pairs with a proof-artifact regen. Tracked: confighub/helm-expt#97; flagged on PR #95.

Strategy lesson: a package-source change and its proof-artifact regen are coupled; CI correctly treats an unregenerated source change as BLOCK. (It also means my F1 PR was incomplete - the namespace fix is right, but it needed the companion regen.)

Phase 2 - Helm equivalence: 20/20 (positive result, not a finding)

On the pristine tree, all 20 TOP20 charts' cub installer render is semantically equivalent to helm template (npm run <chart>:compare → 20/20 PASS; the only cub-only object is the generated Namespace). This is the "same as Helm" half of the thesis, proven across the catalog (read-only, no cluster).

What Wave 1 still PROVED despite F1

F1 is a catalog-content defect, not a pipeline failure. With a coherent render, the full chain - cub installer → ConfigHub units → OCI at oci.hub.confighub.com → Argo (app-of-apps) → running workload - worked end-to-end and three-way agreed. The mechanism is sound; the catalog packaging needs the namespace fix.