Parent runbook: docs/slo/completed/RUNBOOK-hulumi-k8s.md. Read the runbook's Global Execution Rules (especially Rule 0 — the Hulumi-K8s scope contract) + Global Entry Rules before starting.
Goal: After M1, a new workspace package @hulumi/k8s-baseline@1.0.0-pre.1 exists with two shippable components: HardenedHelmRelease (a thin wrapper on @pulumi/kubernetes helm.v3.Release enforcing required version, required repository, instance-name release default, opt-in Fargate-exclusion nodeAffinity, and chart-class-aware timeout) and EksSubnetTagger (writes the three conventional ALB-Controller-discovery tags via aws.ec2.Tag). Closes #38 (subnet tagging), the cross-cutting #44 (release-name default), and half-closes #42 (Fargate affinity arg lands on the wrapper; auto-detection across cluster shape stays an opt-in).
Context: The design record docs/slo/design/hulumi-k8s-surface.md commits the package layout (@hulumi/k8s-baseline separate package, not @hulumi/baseline/k8s subpath), the HardenedHelmRelease API shape, the stable-name + version-pin defaults, and the EksSubnetTagger shape. M1 ships that exact API surface, no more. The components mirror packages/baseline/src/aws/secure-bucket.ts's discipline (pulumi.ComponentResource, child registration via { parent: this }, assertValidTier not used here because tiers are not part of the K8s components' API at v1, tag triple hulumi:component/hulumi:tier?/hulumi:controls? only emitted on resources where it makes sense).
Important design rule: HardenedHelmRelease's release-name default is the ComponentResource instance name verbatim — not the chart name, not a derived value. The Pulumi default of "always add an 8-character random suffix" is the wrong default for IaC reproducibility (issue #44); reverting it is the load-bearing decision M1 ships. Override via explicit releaseName arg. Two HardenedHelmRelease instances with the same instance name in the same namespace WILL collide on helm install — Pulumi's diagnostic surfaces this clearly at preview time. This is a feature, not a bug: collisions surface at IaC review. The decision is irreversible without a pulumi replace (delete + create); the migration cookbook for adopting the new wrapper on existing suffixed releases lands in M5.
Refactor budget: Surgical addition only. New packages/k8s-baseline/ directory containing the package manifest, source, tests, and compatibility table. The only existing files modified are pnpm-workspace.yaml (the workspace already includes packages/* per the existing structure — verify, no edit needed if so), scripts/exact-pin-guard.mjs and scripts/cooling-off-diff.mjs (add @pulumi/kubernetes and @pulumi/eks to the pin-checked + cooling-off-checked lists), docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker (mark M1 in_progress → done), docs/ARCHITECTURE.md (one-paragraph addition describing the new package). No changes to any existing packages/baseline/, packages/policies/, or packages/drift/ source.
| Field | Value |
|---|---|
| Inputs | (Component) new HardenedHelmRelease(name, args) where args: HardenedHelmReleaseArgs requires: chart: string, version: string (no latest, no missing — refuses construction), namespace: pulumi.Input<string>, repository: string (must start with https:// or oci:// — refuses local paths and helm repo lookups). Optional: releaseName?: string (defaults to component instance name verbatim), values?: pulumi.Inputs, daemonSet?: boolean (default false; when true, the wrapper injects an eks.amazonaws.com/compute-type NotIn fargate nodeAffinity into values.affinity if no affinity is already present), excludeFargate?: boolean (default true when daemonSet: true, otherwise no-op), waitTimeoutMs?: number (default 300_000; chart-class table provides higher defaults — Istio: 480_000, set in M2), chartClass?: "istio" | "default" (default "default"; M2 adds "istio" consumers). (Component) new EksSubnetTagger(name, args) where args: EksSubnetTaggerArgs requires: clusterName: string, ownership: "shared" | "owned". Optional: publicSubnetIds?: pulumi.Input<pulumi.Input<string>[]>, privateSubnetIds?: pulumi.Input<pulumi.Input<string>[]>. At least one of the two subnet lists must be supplied — refuses construction if both are absent. |
| Outputs | (Component) HardenedHelmReleaseOutputs exposes releaseName: pulumi.Output<string>, namespace: pulumi.Output<string>, status: pulumi.Output<helm.v3.ReleaseStatus>, chartVersion: pulumi.Output<string> (echoes the input — useful for downstream dependsOn chains and version-skew assertions). (Component) EksSubnetTaggerOutputs exposes tagsApplied: pulumi.Output<{ subnetId: string; key: string; value: string }[]> listing every tag the component wrote. |
| Interfaces touched | New stable surface: @hulumi/k8s-baseline#{HardenedHelmRelease,HardenedHelmReleaseArgs,HardenedHelmReleaseOutputs,HARDENED_HELM_RELEASE_COMPONENT_TYPE,EksSubnetTagger,EksSubnetTaggerArgs,EksSubnetTaggerOutputs,EKS_SUBNET_TAGGER_COMPONENT_TYPE}. Component types: "hulumi:k8s:HardenedHelmRelease", "hulumi:k8s:EksSubnetTagger". The package's first published version is 1.0.0-pre.1 in M1; 1.0.0 lands in M5 alongside the atomic four-package release. |
| Data classification | Internal — the milestone provisions Helm releases and writes EC2 tags on subnets in a sandbox EKS cluster (or kind cluster) during integration tests (no PII, no customer data, no production secrets). HardenedHelmRelease and EksSubnetTagger are declarative IaC — they do not handle user data at runtime. Helm values arg can carry Confidential data (chart secrets); covered by Pulumi's standard pulumi.secret() discipline, not by this milestone's surface. |
| Proactive controls in play | (a) C1 Define Security Requirements — this milestone's design record IS the security requirements record for the K8s variant; the runbook's Rule 0 scope contract pins the surface boundary. (b) C5 Validate All Inputs — HardenedHelmReleaseArgs.version is required and refused if missing or "latest"; repository is refused if it doesn't start with https:// or oci://; EksSubnetTaggerArgs refused if both subnet lists are absent. (c) @hulumi/baseline.aws.SecureBucket (existing precedent) — both new components mirror its ComponentResource discipline (child registration via { parent: this }, conditional tag emission, no implicit defaults that hide policy choices). (d) C10 Handle All Errors and Exceptions — components throw plain Error for input violations (matches existing AWS pattern); no detail-leakage paths. (e) C9 Implement Security Logging and Monitoring — HardenedHelmRelease emits a pulumi.log.warn when the consumer pins to a chart version not in COMPATIBILITY.md; the warning surfaces in pulumi up output, recording the consumer's risk acceptance. |
| Abuse acceptance scenarios | Six BDD rows in the table below cite tm-hulumi-k8s-abuse-N. Slug-keyed: tm-hulumi-k8s-abuse-version-omitted (HardenedHelmRelease refuses construction without version), tm-hulumi-k8s-abuse-version-floating (refuses "latest" and ^/~-prefixed semver — Helm chart versions are exact), tm-hulumi-k8s-abuse-untrusted-repo (refuses repository: "file://…" or non-http(s)/oci paths to prevent local-path supply-chain confusion), tm-hulumi-k8s-abuse-implicit-suffix (release name does NOT receive a random suffix; two same-name same-namespace components collide loudly), tm-hulumi-k8s-abuse-affinity-clobber (daemonSet: true injection refuses to silently overwrite a consumer-supplied affinity block; refuses construction with a clear error), tm-hulumi-k8s-abuse-subnet-empty (EksSubnetTagger refuses if both subnet lists are absent — silently writing no tags is the failure mode that motivated the issue). Threat-model rows trace to the design record's § Decision: HardenedHelmRelease wrapper + § Decision: EksSubnetTagger sections. |
| Files allowed to change | New: packages/k8s-baseline/{package.json,tsconfig.json,tsconfig.build.json,vitest.config.ts,COMPATIBILITY.md}; packages/k8s-baseline/src/{index.ts,hardened-helm-release.ts,hardened-helm-release.args.ts,hardened-helm-release.outputs.ts,eks-subnet-tagger.ts,eks-subnet-tagger.args.ts,eks-subnet-tagger.outputs.ts,compatibility.ts}; packages/k8s-baseline/tests/{hardened-helm-release.test.ts,eks-subnet-tagger.test.ts,compatibility.test.ts}; packages/k8s-baseline/tests/integration/kind/hardened-helm-release.kind.test.ts; docs/slo/runbook-milestones/hulumi-k8s-m1.md (this file — Evidence Log only, in execution); docs/slo/lessons/hulumi-k8s-m1.md; docs/slo/completion/hulumi-k8s-m1.md; docs/components/hardened-helm-release.md (one-line stub); docs/components/eks-subnet-tagger.md (one-line stub). Modified: scripts/exact-pin-guard.mjs; scripts/cooling-off-diff.mjs; docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker; docs/ARCHITECTURE.md (one-paragraph K8s-package addition); pnpm-workspace.yaml IF packages/* is not already covered (verify first); .gitignore IF needed for kind-cluster artifacts. Files outside this milestone's allow-list — REFUSE TO TOUCH: any packages/baseline/src/, packages/policies/src/, packages/drift/src/, any existing skills/, any existing examples/, any other runbook-milestones file, package.json at repo root (workspace metadata only — never add deps there). |
| Files to read before changing anything | docs/slo/completed/RUNBOOK-hulumi-k8s.md (Global Execution Rules + this milestone in full); docs/slo/design/hulumi-k8s-surface.md (entire doc); packages/baseline/src/aws/secure-bucket.ts (pattern precedent); packages/baseline/src/aws/secure-bucket.args.ts; packages/baseline/src/aws/secure-bucket.outputs.ts; packages/baseline/package.json (peer-dep + dev-dep style); packages/baseline/tsconfig.json, tsconfig.build.json, vitest.config.ts; packages/baseline/tests/secure-bucket.test.ts; scripts/exact-pin-guard.mjs; scripts/cooling-off-diff.mjs; docs/cookbooks/psa-baseline-istio-sidecar.md (the cookbook this surface eventually replaces). |
| New files allowed | All "New" entries in Files allowed to change. |
| New dependencies allowed | Runtime peer + dev: @pulumi/kubernetes@^4.x (exact-pin via integrity hash, mirroring @pulumi/aws discipline), @pulumi/eks@^3.x (peer dep only — used for EksSubnetTagger typing if/when the typed args reference EKS types; declared as optional peer with no runtime import to keep the install cost off non-EKS users; decide during implementation — if no typed reference is needed, do not add). Test-only dev: nothing additional in M1 (the kind integration test shells out to kind + kubectl binaries; both are dev-environment expectations, not npm deps). No other runtime deps. |
| Migration allowed | no — additive only. New package; no migration of existing code. |
| Compatibility commitments | HardenedHelmRelease, HardenedHelmReleaseArgs, HardenedHelmReleaseOutputs, HARDENED_HELM_RELEASE_COMPONENT_TYPE, EksSubnetTagger, EksSubnetTaggerArgs, EksSubnetTaggerOutputs, EKS_SUBNET_TAGGER_COMPONENT_TYPE are stable from M1 (no rename in v1.x). The chartClass enum is allowed to GROW (e.g., M2 adds "istio"); existing values do not change. The releaseName default (component instance name verbatim) is load-bearing and irreversible without pulumi replace — the migration cookbook lands in M5. Existing AWS + GitHub interfaces from Hulumi v1.x unchanged. |
| Forbidden shortcuts | (a) NEVER wrap helm.v3.Release such that the wrapper silently injects a random suffix. The whole point of the wrapper is the suffix-free default; if the implementation ends up calling helm.v3.Release with no name set (which Pulumi suffixes by default), the wrapper has failed. (b) NEVER accept version: "latest" or any ^/~/>=-prefixed version. Helm chart versions are exact. The constructor refuses with a clear error. (c) NEVER silently overwrite a consumer-supplied affinity block when injecting Fargate-exclusion. If daemonSet: true AND values.affinity is already set by the consumer, refuse construction with Error("HardenedHelmRelease: cannot inject Fargate-exclusion affinity because values.affinity is already set; merge manually or set excludeFargate: false"). (d) NEVER call child_process.exec, eval, or shell-interpolate user input in component code — argv-based spawning only (existing rule, extended). (e) NEVER read the Helm chart from a file:// path or rely on the consumer's local helm repo list. The wrapper requires repository: string starting with https:// or oci://. (f) NEVER add @pulumi/kubernetes or @pulumi/eks without a 72h/24h cooling-off CI check — scripts/cooling-off-diff.mjs extension lands in this milestone. (g) NEVER import from packages/baseline/src/ other than the Tier re-export from the package's public API (@hulumi/baseline/aws) — and even that import is allowed only if Tier is genuinely needed in M1, which it currently is not. (h) NEVER allow EksSubnetTagger to write zero tags silently. If both publicSubnetIds and privateSubnetIds resolve to empty arrays at apply time, emit a pulumi.log.warn (the consumer's lists may legitimately be conditional on tier — log is the right surface; refuse only at construction time when both are absent at the type level). |
- No
IstioFoundation— that's M2. - No
AlbMeshedHttpEntrypoint— that's M3. - No
KubernetesSecretFromAwsSecretsManagerorRdsCredentialSecret— that's M4. - No
GitHubAppCredential— that's M5. - No
@pulumi/eksClusterwrapper. EVER. (Rule 0.) - No
Linkerd*/LinkerdFoundation/ mesh-agnostic facade. TheMeshFoundationref shape lands in M2 withIstioFoundationas the concrete; nothing in M1 anticipates a second mesh. - No examples package (
examples/k8s-helm-smoke/) — that's M5 launch-readiness work. - No threat-model skill scenarios for K8s — Hulumi K8s is intentionally NOT in the threat-model skill's scenario set at v1 (the scope contract precludes the kind of scenario shape the skill produces; revisit at v1.x if consumer demand emerges).
- No CI integration of the kind test in
weekly-integration.yml— that lands in M5 alongside the release. M1 ships the test file; CI gating is M5. - No license-boundary mappings (no
cis-eks.ts, nocis-kubernetes.ts). The package ships zero mapping tables in M1; if they're added later, they go through the same IDs-only discipline ascis-aws.ts/cis-github.ts.
- Complete the Global Entry Rules in
../RUNBOOK-hulumi-k8s.md. - No
docs/slo/lessons/hulumi-k8s-m0.mdexists. Skip "read prior lessons" with a note in the Evidence Log. - Read the design record
../design/hulumi-k8s-surface.mdend-to-end — every API decision in M1 is committed there. - Read files listed in
Files to read before changing anything. - Copy the Evidence Log template into the milestone's Evidence Log section (already present below — clone the row shape).
- Re-state in working notes the four load-bearing constraints: (i) scope contract — no cluster topology, no IRSA, no mesh / ingress alternatives; (ii)
HardenedHelmRelease's release-name default is the component instance name verbatim — the suffix default reversal is the whole point; (iii)versionandrepositoryare required, nolatest, nofile://; (iv)daemonSet: trueopt-in injects Fargate-exclusion affinity but refuses to clobber a pre-setaffinityblock. - Verify the kind binary is available locally for integration tests:
kind versionshould printkind v0.xor higher; if absent, install viabrew install kind(macOS) or [docs/integration-testing.md] guidance. The kind test skips with a clear message whenkindis not on PATH.
| File | Planned Change |
|---|---|
packages/k8s-baseline/package.json |
NEW: package manifest declaring @pulumi/kubernetes peer + dev deps at exact pin |
packages/k8s-baseline/tsconfig.json |
NEW: extends repo tsconfig.base.json |
packages/k8s-baseline/tsconfig.build.json |
NEW: emit-only build config |
packages/k8s-baseline/vitest.config.ts |
NEW: standard vitest config (no node-pool overrides at v1 — revisit if dynamic-resource clash hits in M4) |
packages/k8s-baseline/COMPATIBILITY.md |
NEW: tested-versions table (initially empty — entries land per chart introduction in M2/M3) |
packages/k8s-baseline/src/index.ts |
NEW: re-exports the two component classes + types + component-type constants |
packages/k8s-baseline/src/hardened-helm-release.ts |
NEW: HardenedHelmRelease extends pulumi.ComponentResource; mirrors secure-bucket.ts shape |
packages/k8s-baseline/src/hardened-helm-release.args.ts |
NEW: HardenedHelmReleaseArgs type |
packages/k8s-baseline/src/hardened-helm-release.outputs.ts |
NEW: HardenedHelmReleaseOutputs type |
packages/k8s-baseline/src/eks-subnet-tagger.ts |
NEW: EksSubnetTagger extends pulumi.ComponentResource writing aws.ec2.Tag per subnet × tag |
packages/k8s-baseline/src/eks-subnet-tagger.args.ts |
NEW: EksSubnetTaggerArgs type |
packages/k8s-baseline/src/eks-subnet-tagger.outputs.ts |
NEW: EksSubnetTaggerOutputs type |
packages/k8s-baseline/src/compatibility.ts |
NEW: typed const TESTED_VERSIONS mapping chart -> version[]; assertVersionTested(chart, version) function emitting pulumi.log.warn when not tested |
packages/k8s-baseline/tests/hardened-helm-release.test.ts |
NEW: Vitest BDD covering happy path, invalid input (missing version, "latest", ^-prefixed, non-http(s)/oci repository, daemonSet+pre-set-affinity), abuse rows |
packages/k8s-baseline/tests/eks-subnet-tagger.test.ts |
NEW: Vitest BDD covering happy path with both subnet lists, public-only, private-only, both-absent refusal, ownership semantics |
packages/k8s-baseline/tests/compatibility.test.ts |
NEW: asserts assertVersionTested warns (not throws) on untested versions; asserts the table parses |
packages/k8s-baseline/tests/integration/kind/hardened-helm-release.kind.test.ts |
NEW: kind-cluster gated test; installs a tiny chart (e.g., bitnami/nginx) via HardenedHelmRelease and asserts the K8s Service.metadata.name matches the wrapper's releaseName (no suffix) |
scripts/exact-pin-guard.mjs |
MODIFY: extend the pin-checked dep list to include @pulumi/kubernetes (and @pulumi/eks if the optional peer is added) |
scripts/cooling-off-diff.mjs |
MODIFY: extend the cooling-off-checked dep list to include @pulumi/kubernetes (and @pulumi/eks if added) |
docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker |
MODIFY: M1 row → in_progress on start, done on exit |
docs/slo/runbook-milestones/hulumi-k8s-m1.md |
MODIFY (during execution only): fill Evidence Log rows |
docs/slo/lessons/hulumi-k8s-m1.md |
NEW (during exit): surprises, decisions, deltas-from-plan |
docs/slo/completion/hulumi-k8s-m1.md |
NEW (during exit): changed files, tests added, docs updated |
docs/components/hardened-helm-release.md |
NEW (one-line stub): "Helm release wrapper enforcing version + repository + stable name; full reference at M5." |
docs/components/eks-subnet-tagger.md |
NEW (one-line stub): "ALB-Controller-discovery tag writer; full reference at M5." |
docs/ARCHITECTURE.md |
MODIFY: append one paragraph describing the new @hulumi/k8s-baseline package and its package-layout rationale (link to design record) |
pnpm-workspace.yaml |
VERIFY (modify only if needed): the existing pattern should already cover packages/* — if not, extend |
.gitignore |
MODIFY (only if needed): add patterns for kind cluster state and Helm chart caches under test tempdirs |
- Verify
pnpm-workspace.yamlcoverspackages/*. Read repo rootpackage.jsonworkspace field — if it listspackages/*(currently listsskills/*andtests/*), extend it; otherwise document in Evidence Log. - Scaffold
packages/k8s-baseline/mirroringpackages/baseline/shape:package.json,tsconfig.json(extends../../tsconfig.base.json),tsconfig.build.json,vitest.config.ts, emptysrc/index.ts, emptytests/dir, emptyCOMPATIBILITY.md. Runpnpm installfrom repo root — it should pick up the new workspace, install@pulumi/kubernetespeer + dev deps, and report success. - Extend
scripts/exact-pin-guard.mjsto include@pulumi/kubernetes(and@pulumi/eksif added) inPULUMI_PACKAGES. Extendscripts/cooling-off-diff.mjslikewise. Run both — expect green now that the new deps are pinned. - Write
packages/k8s-baseline/tests/hardened-helm-release.test.tsmock-runtime tests covering: happy path (valid args yield correct release name, namespace, version), refusal of missingversion, refusal of"latest", refusal of^1.0.0, refusal of non-http(s)/ocirepository, refusal ofdaemonSet: truewhenvalues.affinityis already set, the affinity-injection happy path. Run — expect failures for "module not found". - Implement
HardenedHelmReleaseArgs(interface) +HardenedHelmReleaseOutputs+HardenedHelmRelease(pulumi.ComponentResourceconstructor that validates inputs, builds thehelm.v3.Releasewithname: releaseName ?? componentInstanceName,version,repositoryOpts: { repo: repository },valueswith optional Fargate-affinity injection,wait: true,timeout: waitTimeoutMs / 1000). Re-run — tests should pass. - Write
packages/k8s-baseline/tests/eks-subnet-tagger.test.tsmock-runtime tests covering: happy path (both subnet lists yield 3 tags per subnet), public-only path, private-only path, both-absent refusal, ownership=sharedvsownedtag value. ImplementEksSubnetTagger(constructor walks each subnet × tag-key, writesaws.ec2.Tagwith{ parent: this }). Re-run — tests pass. - Write
packages/k8s-baseline/src/compatibility.tswith emptyTESTED_VERSIONSconst andassertVersionTested(chart, version)that emitspulumi.log.warnif the chart isn't in the table OR the version is not in the chart's tested list. Wire intoHardenedHelmReleaseconstructor. Writepackages/k8s-baseline/tests/compatibility.test.tsasserting the warn-not-throw semantic (using apulumi.logspy). - Wire
packages/k8s-baseline/src/index.tsre-exports. Runpnpm --filter @hulumi/k8s-baseline build && test && typecheck && lint— green. - Write the kind integration test at
packages/k8s-baseline/tests/integration/kind/hardened-helm-release.kind.test.ts: gated onkindbinary present +KIND_E2E=1env var; creates a uniquely-named kind cluster, installsbitnami/nginxviaHardenedHelmRelease(name="hulumi-test-nginx", ...), asserts viakubectl get releasethat the release name is exactlyhulumi-test-nginx(no suffix), tears down. Run locally if kind is installed, otherwise document the skip. - Run the full repo test suite:
pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guard. All green. Update Milestone Tracker, write lessons + completion files.
Feature: @hulumi/k8s-baseline.HardenedHelmRelease enforces version + repository + stable name; EksSubnetTagger writes the three ALB-discovery tags
| Scenario | Category | Given | When | Then | Threat-model row | Control |
|---|---|---|---|---|---|---|
Happy path — HardenedHelmRelease with valid args |
happy path | mock-runtime; chart: "nginx", version: "15.4.4", namespace: "default", repository: "https://charts.bitnami.com/bitnami" |
new HardenedHelmRelease("my-nginx", args) is constructed |
child helm.v3.Release is registered with name: "my-nginx" (NOT "my-nginx-<8charsuffix>"), version: "15.4.4", chart: "nginx", repositoryOpts.repo: "https://charts.bitnami.com/bitnami", wait: true, timeout: 300; outputs expose releaseName: "my-nginx", chartVersion: "15.4.4" |
n/a (happy path) | n/a |
Happy path — explicit releaseName overrides instance name |
happy path | mock-runtime; same as above but releaseName: "my-named" |
new HardenedHelmRelease("instance-name", args) is constructed |
child helm.v3.Release is registered with name: "my-named"; outputs expose releaseName: "my-named" |
n/a (happy path) | n/a |
Happy path — daemonSet: true injects Fargate-exclusion affinity |
happy path | mock-runtime; valid args + daemonSet: true, no values.affinity pre-set |
new HardenedHelmRelease(...) is constructed |
child helm.v3.Release.values.affinity contains nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] of { key: "eks.amazonaws.com/compute-type", operator: "NotIn", values: ["fargate"] } |
n/a (happy path) | n/a |
Happy path — EksSubnetTagger with both subnet lists |
happy path | mock-runtime; clusterName: "test-cluster", publicSubnetIds: ["subnet-aaa", "subnet-bbb"], privateSubnetIds: ["subnet-ccc"], ownership: "shared" |
new EksSubnetTagger(...) is constructed |
3 child aws.ec2.Tag resources registered for subnet-aaa (kubernetes.io/role/elb=1, kubernetes.io/cluster/test-cluster=shared); 3 for subnet-bbb; 3 for subnet-ccc (kubernetes.io/role/internal-elb=1, kubernetes.io/cluster/test-cluster=shared); tagsApplied output lists all 9 |
n/a (happy path) | n/a |
Happy path — EksSubnetTagger private-only |
happy path | mock-runtime; only privateSubnetIds |
new EksSubnetTagger(...) is constructed |
child aws.ec2.Tag resources for internal-elb + cluster ownership only; no kubernetes.io/role/elb tags |
n/a (happy path) | n/a |
Happy path — ownership: "owned" writes =owned value |
happy path | mock-runtime; ownership: "owned" with public + private subnets |
new EksSubnetTagger(...) is constructed |
every kubernetes.io/cluster/<name> tag has value: "owned" (not "shared") |
n/a (happy path) | n/a |
Invalid input — missing version on HardenedHelmRelease |
invalid input | mock-runtime; args without version |
new HardenedHelmRelease(...) is constructed |
constructor throws Error('HardenedHelmRelease: version is required and must be an exact chart version (no "latest", no semver ranges)'); no child resources registered |
n/a (invalid input) | C5 input validation |
Invalid input — empty subnet lists on EksSubnetTagger |
invalid input | mock-runtime; both publicSubnetIds and privateSubnetIds absent |
new EksSubnetTagger(...) is constructed |
constructor throws Error('EksSubnetTagger: at least one of publicSubnetIds or privateSubnetIds must be provided'); no child resources registered |
tm-hulumi-k8s-abuse-subnet-empty |
C5 input validation |
Empty state — EksSubnetTagger with empty arrays at apply time |
empty state | mock-runtime; publicSubnetIds: [], privateSubnetIds: [] (both supplied as empty) |
new EksSubnetTagger(...) is constructed |
constructor succeeds (the empty array is a valid runtime state — e.g., consumer's tier disables public subnets); zero child resources registered; pulumi.log.warn emitted with text containing "EksSubnetTagger" and "no tags written" |
n/a | C9 logging |
Dependency failure — helm.v3.Release provider unconfigured |
partial failure | mock-runtime where the implicit kubernetes.Provider lacks credentials |
new HardenedHelmRelease(...) is constructed |
mock-runtime test passes (no real K8s API call); kind integration test gracefully skips when kind binary is absent or KIND_E2E unset |
n/a | env-var-gating in test setup |
| Abuse case — version omitted refused | abuse case | mock-runtime; args cast through as any to bypass TS, no version field |
new HardenedHelmRelease(...) is constructed |
constructor throws the missing-version error from row "Invalid input — missing version"; no child resources registered |
tm-hulumi-k8s-abuse-version-omitted |
C5 + Forbidden shortcut (b) |
Abuse case — "latest" refused |
abuse case | mock-runtime; version: "latest" |
new HardenedHelmRelease(...) is constructed |
constructor throws Error mentioning "latest"; no child resources registered |
tm-hulumi-k8s-abuse-version-floating |
C5 + Forbidden shortcut (b) |
Abuse case — ^1.0.0 refused |
abuse case | mock-runtime; version: "^1.0.0" |
new HardenedHelmRelease(...) is constructed |
constructor throws Error mentioning "exact"; no child resources registered |
tm-hulumi-k8s-abuse-version-floating |
C5 + Forbidden shortcut (b) |
Abuse case — repository: "file:///tmp/charts/nginx" refused |
abuse case | mock-runtime; valid version + repository: "file:///tmp/charts/nginx" |
new HardenedHelmRelease(...) is constructed |
constructor throws Error mentioning "must start with https:// or oci://"; no child resources registered |
tm-hulumi-k8s-abuse-untrusted-repo |
C5 + Forbidden shortcut (e) |
Abuse case — daemonSet: true with pre-set affinity refused |
abuse case | mock-runtime; valid args + daemonSet: true + values.affinity: { ... } already set |
new HardenedHelmRelease(...) is constructed |
constructor throws Error mentioning "cannot inject Fargate-exclusion affinity because values.affinity is already set"; no child resources registered |
tm-hulumi-k8s-abuse-affinity-clobber |
Forbidden shortcut (c) |
| Abuse case — release name does NOT receive a random suffix | abuse case | mock-runtime; valid args; instance name "my-nginx" | new HardenedHelmRelease("my-nginx", args) is constructed |
child helm.v3.Release.name is exactly "my-nginx" (regex ^my-nginx$); does NOT match ^my-nginx-[0-9a-f]{8}$ |
tm-hulumi-k8s-abuse-implicit-suffix |
Forbidden shortcut (a) |
Schema / compatibility — HardenedHelmReleaseArgs shape lock |
schema / compatibility | packages/k8s-baseline/src/hardened-helm-release.args.ts exists |
tests/skill-bdd/k8s-baseline-args.test.ts (added in this milestone if absent) runs |
HardenedHelmReleaseArgs exports the documented fields with the documented types; chart, version, namespace, repository are required; releaseName, values, daemonSet, excludeFargate, waitTimeoutMs, chartClass are optional; no field is renamed or removed in v1.x |
n/a | type-layer schema lock |
Compatibility — untested chart version emits pulumi.log.warn |
compatibility | mock-runtime; chart: "nginx", version: "0.0.0-untested" (NOT in TESTED_VERSIONS) |
new HardenedHelmRelease(...) is constructed |
pulumi.log.warn is called with text containing the chart + version + a pointer to COMPATIBILITY.md; no Error thrown; child resource still registered |
n/a | C9 logging + Rule 8 |
- All AWS BDD scenarios in
packages/baseline/tests/continue to pass (no rename, no behavioral change). - All GitHub BDD scenarios in
packages/baseline/tests/github/continue to pass. pnpm run lint:license-boundarycontinues to pass on the existing surface.pnpm run lint:exact-pin-guardpasses on the extended dep list (now incl.@pulumi/kubernetes).- Skill
SKILL.mdcontinues to validate against the agentskills.io schema (no skill changes in M1). packages/drift/tests/tla-alignment.test.tscontinues to pass (this milestone does not touchverdict.tsorHulumiDrift.tla).packages/baseline/tests/index.test.ts(or equivalent re-export verification) continues to pass.
-
HardenedHelmRelease,HardenedHelmReleaseArgs,HardenedHelmReleaseOutputs,HARDENED_HELM_RELEASE_COMPONENT_TYPE,EksSubnetTagger,EksSubnetTaggerArgs,EksSubnetTaggerOutputs,EKS_SUBNET_TAGGER_COMPONENT_TYPEdocumented indocs/components/(one-line stubs adequate; full reference doc in M5). -
@pulumi/kubernetesexact-pinned with integrity hash inpackages/k8s-baseline/package.json. -
pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guardgreen on Node 20 LTS. - License header present on every new
.tssource file. - DCO sign-off required on every commit (CI enforcement carries over).
- All existing AWS + GitHub BDD scenarios continue to produce valid output unchanged.
- No
child_process.exec,eval, or shell-interpolation in any new file —tests/no-shell-exec.test.ts(existing) coverspackages/*/src/; extend coverage topackages/k8s-baseline/src/if not already glob-matched. -
packages/k8s-baseline/COMPATIBILITY.mdexists (initially empty — entries land per chart introduction in M2/M3).
Files: packages/k8s-baseline/tests/integration/kind/hardened-helm-release.kind.test.ts.
| E2E Test | What It Proves | Pass Criteria |
|---|---|---|
installs_chart_against_kind_with_stable_release_name |
HardenedHelmRelease works end-to-end against a real cluster |
Test creates a kind cluster hulumi-k8s-m1-<test-id>; installs bitnami/nginx via new HardenedHelmRelease("hulumi-test-nginx", { chart: "nginx", version: "15.4.4", namespace: "default", repository: "https://charts.bitnami.com/bitnami" }); kubectl get release returns exactly one release named hulumi-test-nginx (no suffix); teardown deletes the cluster |
release_name_collision_surfaces_as_helm_install_failure |
The collision-as-feature design intent is verified | Two HardenedHelmRelease instances named "my-nginx" in the same namespace cause the second pulumi up to fail with a Helm cannot re-use a name that is still in use diagnostic; Pulumi diagnostic is clear at preview time |
daemonSet_true_injects_fargate_affinity_in_rendered_manifest |
The Fargate-affinity injection actually reaches the rendered chart | Install a chart known to contain a DaemonSet with daemonSet: true; kubectl get ds <name> -o yaml returns a node affinity matching the documented selector |
pin_guard_catches_pulumi_kubernetes_drift |
scripts/exact-pin-guard.mjs covers the new dep |
Seeded fixture mutating @pulumi/kubernetes integrity hash → pnpm run lint:exact-pin-guard exits non-zero with file:line |
-
pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guard→ all green. - (Optional, requires
kindbinary)KIND_E2E=1 pnpm --filter @hulumi/k8s-baseline test:integration:kind→ integration test green; kind clusterhulumi-k8s-m1-*does not persist after the run. - In a Pulumi program importing
@hulumi/k8s-baseline:new HardenedHelmRelease("foo", { ... no version ... } as any)causespulumi previewto fail with the documented missing-version error. - In a Pulumi program:
new EksSubnetTagger("foo", { clusterName: "x", ownership: "shared" } as any)causespulumi previewto fail with the documented missing-subnets error. -
git statusshows no untracked test artifacts. -
.gitignorecovers any new generated files (e.g. kind cluster checkpoints under~/.config/kindand~/.cache/helmare user-side, not repo).
| Step | Command / Check | Expected Result | Actual Result | Pass/Fail | Notes |
|---|---|---|---|---|---|
| Baseline tests | pnpm -r build && pnpm -r test |
green pre-M1 | filled during execution | pending | Captures pre-M1 baseline so any regression is attributable to M1 |
| Workspace verification | grep packages in package.json and pnpm-workspace.yaml |
one of them lists packages/* |
filled during execution | pending | Establishes whether the workspace already covers the new package |
| Package skeleton | ls packages/k8s-baseline/ |
files present per Files Allowed To Change table | filled during execution | pending | After scaffolding |
| Pin-guard extension | pnpm run lint:exact-pin-guard |
OK with @pulumi/kubernetes listed |
filled during execution | pending | Confirms @pulumi/kubernetes integrity hash captured |
| Cooling-off-diff extension | node scripts/cooling-off-diff.mjs --dry |
@pulumi/kubernetes in PULUMI_PACKAGES |
filled during execution | pending | Future bumps subject to cooling-off |
| BDD tests created | pnpm --filter @hulumi/k8s-baseline test (pre-impl) |
fail with module-not-found / wrong-shape errors | filled during execution | pending | Pre-implementation failure shape captured |
HardenedHelmRelease impl |
filesystem | source files present and re-exported from index.ts |
filled during execution | pending | After implementation |
EksSubnetTagger impl |
filesystem | source files present and re-exported from index.ts |
filled during execution | pending | After implementation |
compatibility.ts impl |
pnpm --filter @hulumi/k8s-baseline test -- compatibility |
warn-not-throw asserted | filled during execution | pending | After compatibility test passes |
| Mock-runtime BDD | pnpm --filter @hulumi/k8s-baseline test |
all BDD rows pass | filled during execution | pending | Every row from the BDD table covered |
| Build / typecheck / lint | pnpm -r build && pnpm -r typecheck && pnpm -r lint |
green | filled during execution | pending | All packages clean |
| Kind integration test | KIND_E2E=1 pnpm --filter @hulumi/k8s-baseline test:integration:kind |
green or skipped | filled during execution | pending | Skip cleanly if kind binary absent |
| License-boundary lint | pnpm run lint:license-boundary |
OK | filled during execution | pending | Existing lint scope unchanged in M1 |
Smoke — pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint |
terminal | all green | filled during execution | pending | Final regression sweep |
| Test artifact cleanup | git status --short |
only intentional new files + modified tracker | filled during execution | pending | No transient test artifacts |
| .gitignore review | existing .gitignore covers Node/pnpm/Vitest/TLA+ |
no change needed unless kind drops state in repo | filled during execution | pending | Adjust if kind dumps cluster state in ./ |
- All BDD scenarios pass (mock-runtime always; kind integration when binary present).
- All E2E runtime validation tests pass.
pnpm -r testgreen;pnpm -r typecheckgreen;pnpm -r lintgreen;pnpm run lint:license-boundarygreen;pnpm run lint:exact-pin-guardgreen.- Smoke tests checked off.
- Compatibility checklist complete.
- No forbidden shortcuts present.
git statusclean..gitignorecovers all new generated files.- All existing AWS + GitHub BDD scenarios produce valid output unchanged (regression-tested).
docs/slo/lessons/hulumi-k8s-m1.mdwritten (incl. the workspace-extension decision and any kind-test surprises).docs/slo/completion/hulumi-k8s-m1.mdwritten.- Milestone Tracker in
docs/slo/completed/RUNBOOK-hulumi-k8s.mdupdated todone.
docs/slo/completed/RUNBOOK-hulumi-k8s.mdMilestone Tracker → M1done.docs/slo/completed/RUNBOOK-hulumi-k8s.mdComponent Summary Table — verify M1 row matches what was actually shipped.docs/components/hardened-helm-release.md— one-line stub if not present (full reference doc in M5).docs/components/eks-subnet-tagger.md— one-line stub if not present (full reference doc in M5).docs/ARCHITECTURE.md— one-paragraph description of the new@hulumi/k8s-baselinepackage and the package-layout rationale (link to design record).docs/issue-candidates.md— strike #38 and #44 (filed → shipped); update the K8s table accordingly.
- This milestone ships no
IstioFoundation, no mesh entrypoint, no secret extraction, no GitHub App component — those are M2 / M3 / M4 / M5. - Prior-lessons coverage category does not apply (greenfield for the K8s variant).
- The
releaseNamedefault (component instance name verbatim) is the single most important design decision in this milestone — it must be recorded in the lessons file as a deliberate reversal of the Pulumi default and as the load-bearing reasonHardenedHelmReleaseexists at all. - The kind integration test is gated on
kindbinary present +KIND_E2E=1env var. CI in this milestone runs only mock-runtime BDD; the kind suite is exercised in M5's release readiness smoke + the weekly-integration workflow extension.