@intentius/chant-lexicon-k8s 0.41.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Typed step-builders for the k8s lexicon's verbs — the same ergonomic sugar
3
+ * the aws lexicon's `components/builders.ts` offers (#658), reusing the
4
+ * exported `step` projection from `@intentius/chant/components`.
5
+ *
6
+ * `kubectl-apply` is a `needs-opt-out` capability (a server-side apply keeps
7
+ * no previous object state, so there is nothing native to roll back to), which
8
+ * means COMP003 requires every step of it to carry a `noRollback: "<reason>"`,
9
+ * a component-level `rollback` phase, or a sibling safety step. The builder
10
+ * admits `noRollback` directly so the common opt-out reads as one typed call
11
+ * instead of a raw object literal.
12
+ */
13
+ import type { KubectlApplyInput } from "./kubectl-apply.js";
14
+ export declare const kubectlApply: (input: KubectlApplyInput & {
15
+ noRollback?: string;
16
+ }) => import("@intentius/chant/components").Step;
17
+ //# sourceMappingURL=builders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/components/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,eAAO,MAAM,YAAY;iBAA2C,MAAM;gDAAoB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
2
+ export declare const K8S_VERB_FAMILIES: {
3
+ readonly apply: readonly ["kubectl-apply"];
4
+ };
5
+ export declare const k8sCapabilityPlugin: CapabilityPlugin;
6
+ //# sourceMappingURL=capability-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAGtF,eAAO,MAAM,iBAAiB;;CAEpB,CAAC;AAEX,eAAO,MAAM,mBAAmB,EAAE,gBASjC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The k8s lexicon's component/release surface: the `kubectl-apply` capability,
3
+ * its typed step-builder, and the `k8sCapabilityPlugin` core loads when a
4
+ * project declares `lexicons: ["k8s"]` (#1495). Component authors import the
5
+ * builders from here (`@intentius/chant-lexicon-k8s/components`), the way AWS
6
+ * verbs come from `@intentius/chant-lexicon-aws/components`.
7
+ *
8
+ * Everything exported here is on the build path (#1074): nothing may
9
+ * statically import the Kubernetes API client chain (`../op/activities/*`,
10
+ * `../kube/*`) — `kubectl-apply.ts` reaches its applier by dynamic import
11
+ * inside `run()`, and this module must stay that clean.
12
+ */
13
+ export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin.js";
14
+ export * from "./builders.js";
15
+ export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput, type KubectlApplyOutcome, } from "./kubectl-apply.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `kubectl-apply` — the Kubernetes apply leaf for the component model
3
+ * (#1495 piece 2).
4
+ *
5
+ * The apply itself already exists: chant's server-side apply as the field
6
+ * manager `chant:<stack>` with the marker-scoped owned-only prune (#1075),
7
+ * reachable from Ops and from `ApplyOp({ target: "kubectl" })`. What was
8
+ * missing is a *capability*, so a component whose deploy is a Kubernetes
9
+ * manifest had nothing to compose but a `shell` step — and, downstream, no
10
+ * deploy unit for `chant components status --live` to observe (the walk asks
11
+ * the step's `stack`, and a shell step has none to name).
12
+ *
13
+ * `stack` doubles as both the ownership identity the apply stamps
14
+ * (`chant:<stack>`, the prune selector) and the deploy unit the status walk
15
+ * reads (core `components/deploy-units.ts`) — one name, both jobs, the same
16
+ * pairing `cfn-deploy` has with its stack.
17
+ */
18
+ import type { Capability } from "@intentius/chant/components/capability";
19
+ /**
20
+ * Structural mirrors of the activity module's argument/result shapes.
21
+ *
22
+ * Deliberately NOT imported from ../op/activities/kubectl — not even as
23
+ * `import type`: the #1074 boundary test walks type-only imports too, and a
24
+ * static reference here would put the API-client chain on the build path this
25
+ * module is loaded from (the lexicon entry point). The applier is reached by
26
+ * dynamic import inside run(), the same mechanism plugin.ts and the kube verb
27
+ * modules use; TypeScript checks these shapes against the real ones at the
28
+ * call site, so drift is a compile error there, not a silent mismatch.
29
+ */
30
+ interface AppliedRef {
31
+ apiVersion: string;
32
+ kind: string;
33
+ name: string;
34
+ namespace?: string;
35
+ }
36
+ /** What the apply did — the activity's own result shape. */
37
+ export interface KubectlApplyOutcome {
38
+ /** The field manager every object was applied as. */
39
+ fieldManager: string;
40
+ applied: AppliedRef[];
41
+ /** Objects deleted because they carried chant's marker and are no longer declared. */
42
+ pruned: AppliedRef[];
43
+ }
44
+ interface ApplierArgs {
45
+ manifest: string;
46
+ environment?: string;
47
+ stack?: string;
48
+ context?: string;
49
+ deleteMode?: "never" | "owned-only" | "gated";
50
+ }
51
+ type Applier = (args: ApplierArgs) => Promise<KubectlApplyOutcome>;
52
+ export interface KubectlApplyInput {
53
+ /** Path to a manifest file, or a directory of them. */
54
+ manifest: string;
55
+ /**
56
+ * The deploy unit / ownership stack. Optional: omitted derives the field
57
+ * manager from the project's `ownership.stack` as the Op activity does —
58
+ * but then the unit is invisible to `components status --live`, which only
59
+ * reads a literal on the step. Name it.
60
+ */
61
+ stack?: string;
62
+ /** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
63
+ context?: string;
64
+ /**
65
+ * What happens to chant-owned objects no longer in the manifest — the same
66
+ * vocabulary the Op activity and `ApplyOp` use. Default `never`; the prune
67
+ * is always marker-scoped, so an object chant did not stamp is never a
68
+ * candidate.
69
+ */
70
+ delete?: "never" | "owned-only";
71
+ }
72
+ /** Factory with an injectable applier, so tests assert the delegation without
73
+ * a cluster — the same seam `createGenerateSbomCapability` uses. The default
74
+ * applier is resolved by dynamic import on first run, keeping the API-client
75
+ * chain off the build path (#1074). */
76
+ export declare function createKubectlApplyCapability(apply?: Applier): Capability<KubectlApplyInput, KubectlApplyOutcome>;
77
+ export declare const kubectlApplyCapability: Capability<KubectlApplyInput, KubectlApplyOutcome>;
78
+ export {};
79
+ //# sourceMappingURL=kubectl-apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kubectl-apply.d.ts","sourceRoot":"","sources":["../../src/components/kubectl-apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,wCAAwC,CAAC;AAExF;;;;;;;;;;GAUG;AACH,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;CAC/C;AAED,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;CACjC;AAED;;;uCAGuC;AACvC,wBAAgB,4BAA4B,CAC1C,KAAK,CAAC,EAAE,OAAO,GACd,UAAU,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAkBpD;AAED,eAAO,MAAM,sBAAsB,oDAAiC,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Deploy-unit status for Kubernetes (#1495 piece 3).
3
+ *
4
+ * A Kubernetes deploy unit *is* a label selector: every object chant applies
5
+ * carries its stack identity in the labels the serializer stamps
6
+ * (`app.kubernetes.io/managed-by=chant`, `chant.intentius.io/stack=<stack>` —
7
+ * see core's ownership marking). So "is this unit present" is one selector
8
+ * query, and "is it healthy" is whether every matching workload its controller
9
+ * reports on is Ready. No new identity concept — this reads back exactly what
10
+ * `kubectl-apply` (piece 2) and the serializer already write.
11
+ *
12
+ * The selector deliberately omits `chant.intentius.io/env`: `ownership.env` is
13
+ * a config identity stamped at synthesis, not necessarily the environment name
14
+ * a caller observes with, and a mismatch would read a deployed unit as absent
15
+ * — the failure mode the tri-state exists to prevent.
16
+ *
17
+ * The sweep is bounded to the kinds chant's own k8s surface deploys as
18
+ * stack-scoped objects (workloads + the service/config plumbing around them).
19
+ * A CRD-only unit is out of this bound and reports absent rather than null —
20
+ * a real limit, stated here rather than papered over; widening the sweep to
21
+ * discovery-driven kinds is a follow-up, not a silent claim.
22
+ */
23
+ import type { StackStatusObservation } from "@intentius/chant/lexicon";
24
+ import { type K8sConnector } from "./api/connect.js";
25
+ export declare function describeStackStatus(options: {
26
+ environment: string;
27
+ stack: string;
28
+ }, connect?: K8sConnector): Promise<StackStatusObservation | null>;
29
+ //# sourceMappingURL=describe-stack-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-stack-status.d.ts","sourceRoot":"","sources":["../src/describe-stack-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAgCvE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC/C,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAuCxC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { k8sSerializer } from "./serializer.js";
2
2
  export { k8sPlugin } from "./plugin.js";
3
+ export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./components/capability-plugin.js";
4
+ export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput } from "./components/kubectl-apply.js";
3
5
  export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
4
6
  export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels.js";
5
7
  export { K8sLabels, K8sAnnotations } from "./variables.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "4c4e0afb787bdb40c3ecae9bcb1018002a85bcd41dcbf5ac12478d1ba092c3d6",
4
+ "manifest.json": "df06efa4031426355eb12f159a646012593f5bc9d4b4df424b453dde81aab0be",
5
5
  "meta.json": "c0b141b882c51483c1d1aead748a0eb171aceca7432edd2a0404b062bf6d7831",
6
6
  "types/index.d.ts": "3be2af7494aca94adfa16269cb9c4e7a155529872207a3d70b0c4d39914f9eec",
7
7
  "rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
@@ -50,5 +50,5 @@
50
50
  "skills/chant-k8s-aks.md": "e18f0e2b055f72cd7a37deaf258d7027c2d4d3e286e8fd4975b27a1f981a3ad9",
51
51
  "skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5"
52
52
  },
53
- "composite": "5f8423c25fc4a50e91f0e7f2a8a8064fc8cc36b150f4b0c630a6cc56e0924995"
53
+ "composite": "58171232438afe93a43c83a5685856c5992179242282773d1cc41bea86ee7c41"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AAwBjG,eAAO,MAAM,SAAS,EAAE,aA6mBvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AAwBjG,eAAO,MAAM,SAAS,EAAE,aAynBvB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA2IlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UA+I3B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA6MlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UA8I3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -44,6 +44,11 @@
44
44
  "types": "./dist/detect.d.ts",
45
45
  "default": "./src/detect.ts"
46
46
  },
47
+ "./components": {
48
+ "development": "./src/components/index.ts",
49
+ "types": "./dist/components/index.d.ts",
50
+ "default": "./src/components/index.ts"
51
+ },
47
52
  "./lint/post-synth": {
48
53
  "development": "./src/lint/post-synth/index.ts",
49
54
  "types": "./dist/lint/post-synth/index.d.ts",
@@ -79,7 +84,7 @@
79
84
  },
80
85
  "peerDependencies": {
81
86
  "zod": "^4.3.6",
82
- "@intentius/chant": "^0.41.0",
87
+ "@intentius/chant": "^0.41.1",
83
88
  "typescript": "^5.9.3"
84
89
  }
85
90
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Typed step-builders for the k8s lexicon's verbs — the same ergonomic sugar
3
+ * the aws lexicon's `components/builders.ts` offers (#658), reusing the
4
+ * exported `step` projection from `@intentius/chant/components`.
5
+ *
6
+ * `kubectl-apply` is a `needs-opt-out` capability (a server-side apply keeps
7
+ * no previous object state, so there is nothing native to roll back to), which
8
+ * means COMP003 requires every step of it to carry a `noRollback: "<reason>"`,
9
+ * a component-level `rollback` phase, or a sibling safety step. The builder
10
+ * admits `noRollback` directly so the common opt-out reads as one typed call
11
+ * instead of a raw object literal.
12
+ */
13
+
14
+ import { step } from "@intentius/chant/components";
15
+ import type { KubectlApplyInput } from "./kubectl-apply";
16
+
17
+ export const kubectlApply = step<KubectlApplyInput & { noRollback?: string }>("kubectl-apply");
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `k8sCapabilityPlugin` — the k8s lexicon's capability plugin (#1495 piece 2).
3
+ *
4
+ * One leaf for now: `kubectl-apply`, the shared apply verb Kubernetes levels
5
+ * everything to (docs/components/cloud-boundary calls it the one portable
6
+ * apply family). Contributed through core's `CapabilityPlugin` contract the
7
+ * same way the aws lexicon contributes `cfn-deploy` — a project declaring
8
+ * `lexicons: ["k8s"]` gets the verb registered automatically when it runs
9
+ * components.
10
+ */
11
+ import type { Capability } from "@intentius/chant/components/capability";
12
+ import type { CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
13
+ import { kubectlApplyCapability } from "./kubectl-apply";
14
+
15
+ export const K8S_VERB_FAMILIES = {
16
+ apply: ["kubectl-apply"],
17
+ } as const;
18
+
19
+ export const k8sCapabilityPlugin: CapabilityPlugin = {
20
+ name: "k8s",
21
+ version: "0.41.0",
22
+ capabilities(): Array<Capability<never, unknown>> {
23
+ return [kubectlApplyCapability as Capability<never, unknown>];
24
+ },
25
+ families(): Record<string, readonly string[]> {
26
+ return K8S_VERB_FAMILIES;
27
+ },
28
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The k8s lexicon's component/release surface: the `kubectl-apply` capability,
3
+ * its typed step-builder, and the `k8sCapabilityPlugin` core loads when a
4
+ * project declares `lexicons: ["k8s"]` (#1495). Component authors import the
5
+ * builders from here (`@intentius/chant-lexicon-k8s/components`), the way AWS
6
+ * verbs come from `@intentius/chant-lexicon-aws/components`.
7
+ *
8
+ * Everything exported here is on the build path (#1074): nothing may
9
+ * statically import the Kubernetes API client chain (`../op/activities/*`,
10
+ * `../kube/*`) — `kubectl-apply.ts` reaches its applier by dynamic import
11
+ * inside `run()`, and this module must stay that clean.
12
+ */
13
+
14
+ export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin";
15
+ export * from "./builders";
16
+ export {
17
+ kubectlApplyCapability,
18
+ createKubectlApplyCapability,
19
+ type KubectlApplyInput,
20
+ type KubectlApplyOutcome,
21
+ } from "./kubectl-apply";
@@ -0,0 +1,33 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createKubectlApplyCapability, k8sCapabilityPlugin } from "../index";
3
+ import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
4
+
5
+ describe("kubectl-apply capability (#1495 piece 2)", () => {
6
+ test("the plugin satisfies the CapabilityPlugin contract and registers the verb", () => {
7
+ expect(isCapabilityPlugin(k8sCapabilityPlugin)).toBe(true);
8
+ const kinds = k8sCapabilityPlugin.capabilities().map((c) => c.kind);
9
+ expect(kinds).toContain("kubectl-apply");
10
+ });
11
+
12
+ test("run delegates to the server-side apply with the component's env and the step's stack", async () => {
13
+ let seen: unknown;
14
+ const cap = createKubectlApplyCapability(async (args) => {
15
+ seen = args;
16
+ return { applied: [], pruned: [], fieldManager: "chant:kubemicrovm-ops" };
17
+ });
18
+ await cap.run({ env: "dev", component: "workload" }, { manifest: "k8s.yaml", stack: "kubemicrovm-ops", delete: "owned-only" });
19
+ expect(seen).toEqual({ manifest: "k8s.yaml", environment: "dev", stack: "kubemicrovm-ops", deleteMode: "owned-only" });
20
+ });
21
+
22
+ test("a mutating verb with no safe undo declares needs-opt-out for COMP003", () => {
23
+ expect(createKubectlApplyCapability().rollbackPolicy).toBe("needs-opt-out");
24
+ });
25
+
26
+ test("the stack field is the deploy unit core's status walk reads (#1495 piece 1)", async () => {
27
+ const { deployUnits } = await import("@intentius/chant/components/deploy-units");
28
+ const units = deployUnits([
29
+ { phase: "Apply", steps: [{ kind: "kubectl-apply", manifest: "k8s.yaml", stack: "kubemicrovm-ops" }] } as never,
30
+ ]);
31
+ expect(units).toEqual([{ unit: "kubemicrovm-ops", lexicon: "k8s" }]);
32
+ });
33
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `kubectl-apply` — the Kubernetes apply leaf for the component model
3
+ * (#1495 piece 2).
4
+ *
5
+ * The apply itself already exists: chant's server-side apply as the field
6
+ * manager `chant:<stack>` with the marker-scoped owned-only prune (#1075),
7
+ * reachable from Ops and from `ApplyOp({ target: "kubectl" })`. What was
8
+ * missing is a *capability*, so a component whose deploy is a Kubernetes
9
+ * manifest had nothing to compose but a `shell` step — and, downstream, no
10
+ * deploy unit for `chant components status --live` to observe (the walk asks
11
+ * the step's `stack`, and a shell step has none to name).
12
+ *
13
+ * `stack` doubles as both the ownership identity the apply stamps
14
+ * (`chant:<stack>`, the prune selector) and the deploy unit the status walk
15
+ * reads (core `components/deploy-units.ts`) — one name, both jobs, the same
16
+ * pairing `cfn-deploy` has with its stack.
17
+ */
18
+ import type { Capability, DeployContext } from "@intentius/chant/components/capability";
19
+
20
+ /**
21
+ * Structural mirrors of the activity module's argument/result shapes.
22
+ *
23
+ * Deliberately NOT imported from ../op/activities/kubectl — not even as
24
+ * `import type`: the #1074 boundary test walks type-only imports too, and a
25
+ * static reference here would put the API-client chain on the build path this
26
+ * module is loaded from (the lexicon entry point). The applier is reached by
27
+ * dynamic import inside run(), the same mechanism plugin.ts and the kube verb
28
+ * modules use; TypeScript checks these shapes against the real ones at the
29
+ * call site, so drift is a compile error there, not a silent mismatch.
30
+ */
31
+ interface AppliedRef {
32
+ apiVersion: string;
33
+ kind: string;
34
+ name: string;
35
+ namespace?: string;
36
+ }
37
+
38
+ /** What the apply did — the activity's own result shape. */
39
+ export interface KubectlApplyOutcome {
40
+ /** The field manager every object was applied as. */
41
+ fieldManager: string;
42
+ applied: AppliedRef[];
43
+ /** Objects deleted because they carried chant's marker and are no longer declared. */
44
+ pruned: AppliedRef[];
45
+ }
46
+
47
+ interface ApplierArgs {
48
+ manifest: string;
49
+ environment?: string;
50
+ stack?: string;
51
+ context?: string;
52
+ deleteMode?: "never" | "owned-only" | "gated";
53
+ }
54
+
55
+ type Applier = (args: ApplierArgs) => Promise<KubectlApplyOutcome>;
56
+
57
+ export interface KubectlApplyInput {
58
+ /** Path to a manifest file, or a directory of them. */
59
+ manifest: string;
60
+ /**
61
+ * The deploy unit / ownership stack. Optional: omitted derives the field
62
+ * manager from the project's `ownership.stack` as the Op activity does —
63
+ * but then the unit is invisible to `components status --live`, which only
64
+ * reads a literal on the step. Name it.
65
+ */
66
+ stack?: string;
67
+ /** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
68
+ context?: string;
69
+ /**
70
+ * What happens to chant-owned objects no longer in the manifest — the same
71
+ * vocabulary the Op activity and `ApplyOp` use. Default `never`; the prune
72
+ * is always marker-scoped, so an object chant did not stamp is never a
73
+ * candidate.
74
+ */
75
+ delete?: "never" | "owned-only";
76
+ }
77
+
78
+ /** Factory with an injectable applier, so tests assert the delegation without
79
+ * a cluster — the same seam `createGenerateSbomCapability` uses. The default
80
+ * applier is resolved by dynamic import on first run, keeping the API-client
81
+ * chain off the build path (#1074). */
82
+ export function createKubectlApplyCapability(
83
+ apply?: Applier,
84
+ ): Capability<KubectlApplyInput, KubectlApplyOutcome> {
85
+ return {
86
+ kind: "kubectl-apply",
87
+ // A server-side apply has no native undo (the previous object state is not
88
+ // kept by the API server), so COMP003 requires the component to acknowledge
89
+ // the compensation gap — the same posture as s3-sync/run-migration.
90
+ rollbackPolicy: "needs-opt-out",
91
+ async run(ctx: DeployContext, input: KubectlApplyInput): Promise<KubectlApplyOutcome> {
92
+ const applier: Applier = apply ?? (await import("../op/activities/kubectl")).applyManifest;
93
+ return applier({
94
+ manifest: input.manifest,
95
+ environment: ctx.env,
96
+ ...(input.stack !== undefined ? { stack: input.stack } : {}),
97
+ ...(input.context !== undefined ? { context: input.context } : {}),
98
+ ...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
99
+ });
100
+ },
101
+ };
102
+ }
103
+
104
+ export const kubectlApplyCapability = createKubectlApplyCapability();
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { describeStackStatus } from "./describe-stack-status";
3
+ import type { K8sConnector } from "./api/connect";
4
+
5
+ /** A connector whose client answers list() from a canned map keyed by kind. */
6
+ function fakeConnector(byKind: Record<string, unknown[]>, opts: { failKinds?: string[]; recordSelectors?: string[] } = {}): K8sConnector {
7
+ return (async () => ({
8
+ client: {
9
+ async list(selector: { kind: string }, listOpts?: { labelSelector?: string }) {
10
+ if (listOpts?.labelSelector && opts.recordSelectors) opts.recordSelectors.push(listOpts.labelSelector);
11
+ if (opts.failKinds?.includes(selector.kind)) throw new Error("rbac denied");
12
+ return byKind[selector.kind] ?? [];
13
+ },
14
+ },
15
+ })) as unknown as K8sConnector;
16
+ }
17
+
18
+ const readyDeployment = {
19
+ kind: "Deployment",
20
+ metadata: { name: "cc-api", labels: { "chant.intentius.io/stack": "cc" } },
21
+ status: { replicas: 2, readyReplicas: 2 },
22
+ };
23
+ const laggingDeployment = { ...readyDeployment, status: { replicas: 2, readyReplicas: 1 } };
24
+ const service = { kind: "Service", metadata: { name: "cc-api" } };
25
+
26
+ describe("k8s describeStackStatus (#1495 piece 3)", () => {
27
+ test("selects on the labels chant's serializer stamps", async () => {
28
+ const selectors: string[] = [];
29
+ await describeStackStatus({ environment: "local", stack: "kubemicrovm-ops" }, fakeConnector({}, { recordSelectors: selectors }));
30
+ expect(selectors[0]).toBe("app.kubernetes.io/managed-by=chant,chant.intentius.io/stack=kubemicrovm-ops");
31
+ });
32
+
33
+ test("present + healthy when every matching workload is ready", async () => {
34
+ const out = await describeStackStatus({ environment: "local", stack: "cc" }, fakeConnector({ Deployment: [readyDeployment], Service: [service] }));
35
+ expect(out).toEqual({ stack: "cc", present: true, status: "1/1 workloads ready", healthy: true });
36
+ });
37
+
38
+ test("present but unhealthy when a workload lags its replica count", async () => {
39
+ const out = await describeStackStatus({ environment: "local", stack: "cc" }, fakeConnector({ Deployment: [laggingDeployment] }));
40
+ expect(out).toMatchObject({ present: true, healthy: false });
41
+ });
42
+
43
+ test("presence-only objects report present and assert nothing about health", async () => {
44
+ const out = await describeStackStatus({ environment: "local", stack: "cc" }, fakeConnector({ Service: [service] }));
45
+ expect(out).toEqual({ stack: "cc", present: true, status: "1 objects present", healthy: true });
46
+ });
47
+
48
+ test("zero matches is the pre-first-apply state — absent, not indeterminate", async () => {
49
+ const out = await describeStackStatus({ environment: "local", stack: "cc" }, fakeConnector({}));
50
+ expect(out).toEqual({ stack: "cc", present: false });
51
+ });
52
+
53
+ test("one kind's read failing does not fail the sweep", async () => {
54
+ const out = await describeStackStatus(
55
+ { environment: "local", stack: "cc" },
56
+ fakeConnector({ Deployment: [readyDeployment] }, { failKinds: ["Ingress", "CronJob"] }),
57
+ );
58
+ expect(out).toMatchObject({ present: true, healthy: true });
59
+ });
60
+
61
+ test("no cluster binding is indeterminate — null, never a confident absence", async () => {
62
+ const broken: K8sConnector = (async () => {
63
+ throw new Error("no context bound for env");
64
+ }) as unknown as K8sConnector;
65
+ expect(await describeStackStatus({ environment: "local", stack: "cc" }, broken)).toBeNull();
66
+ });
67
+
68
+ test("every read failing is indeterminate too", async () => {
69
+ const out = await describeStackStatus(
70
+ { environment: "local", stack: "cc" },
71
+ fakeConnector({}, { failKinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret", "ServiceAccount", "Namespace", "Ingress", "PodDisruptionBudget", "HorizontalPodAutoscaler", "CronJob"] }),
72
+ );
73
+ expect(out).toBeNull();
74
+ });
75
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Deploy-unit status for Kubernetes (#1495 piece 3).
3
+ *
4
+ * A Kubernetes deploy unit *is* a label selector: every object chant applies
5
+ * carries its stack identity in the labels the serializer stamps
6
+ * (`app.kubernetes.io/managed-by=chant`, `chant.intentius.io/stack=<stack>` —
7
+ * see core's ownership marking). So "is this unit present" is one selector
8
+ * query, and "is it healthy" is whether every matching workload its controller
9
+ * reports on is Ready. No new identity concept — this reads back exactly what
10
+ * `kubectl-apply` (piece 2) and the serializer already write.
11
+ *
12
+ * The selector deliberately omits `chant.intentius.io/env`: `ownership.env` is
13
+ * a config identity stamped at synthesis, not necessarily the environment name
14
+ * a caller observes with, and a mismatch would read a deployed unit as absent
15
+ * — the failure mode the tri-state exists to prevent.
16
+ *
17
+ * The sweep is bounded to the kinds chant's own k8s surface deploys as
18
+ * stack-scoped objects (workloads + the service/config plumbing around them).
19
+ * A CRD-only unit is out of this bound and reports absent rather than null —
20
+ * a real limit, stated here rather than papered over; widening the sweep to
21
+ * discovery-driven kinds is a follow-up, not a silent claim.
22
+ */
23
+ import type { StackStatusObservation } from "@intentius/chant/lexicon";
24
+ import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
25
+ import type { K8sObject } from "@intentius/chant-k8s-client";
26
+ import { defaultK8sConnector, type K8sConnector } from "./api/connect";
27
+
28
+ /** The kinds a chant-applied stack's objects land as — the sweep's bound. */
29
+ const SWEEP_KINDS: ReadonlyArray<{ apiVersion: string; kind: string }> = [
30
+ { apiVersion: "apps/v1", kind: "Deployment" },
31
+ { apiVersion: "apps/v1", kind: "StatefulSet" },
32
+ { apiVersion: "apps/v1", kind: "DaemonSet" },
33
+ { apiVersion: "v1", kind: "Service" },
34
+ { apiVersion: "v1", kind: "ConfigMap" },
35
+ { apiVersion: "v1", kind: "Secret" },
36
+ { apiVersion: "v1", kind: "ServiceAccount" },
37
+ { apiVersion: "v1", kind: "Namespace" },
38
+ { apiVersion: "networking.k8s.io/v1", kind: "Ingress" },
39
+ { apiVersion: "policy/v1", kind: "PodDisruptionBudget" },
40
+ { apiVersion: "autoscaling/v2", kind: "HorizontalPodAutoscaler" },
41
+ { apiVersion: "batch/v1", kind: "CronJob" },
42
+ ];
43
+
44
+ /** A workload is healthy when its controller reports every replica ready; a
45
+ * kind with no readiness story (a ConfigMap, a Service) asserts nothing. */
46
+ function objectHealthy(obj: K8sObject): boolean | undefined {
47
+ const status = obj.status as { replicas?: unknown; readyReplicas?: unknown; desiredNumberScheduled?: unknown; numberReady?: unknown } | undefined;
48
+ if (!status) return undefined;
49
+ if (typeof status.replicas === "number") {
50
+ return (typeof status.readyReplicas === "number" ? status.readyReplicas : 0) >= status.replicas;
51
+ }
52
+ if (typeof status.desiredNumberScheduled === "number") {
53
+ return (typeof status.numberReady === "number" ? status.numberReady : 0) >= status.desiredNumberScheduled;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ export async function describeStackStatus(
59
+ options: { environment: string; stack: string },
60
+ connect: K8sConnector = defaultK8sConnector,
61
+ ): Promise<StackStatusObservation | null> {
62
+ const selector =
63
+ `${LABEL_OWNERSHIP_KEYS.managedBy}=${OWNERSHIP_MANAGED_BY_VALUE},` +
64
+ `${LABEL_OWNERSHIP_KEYS.stack}=${options.stack}`;
65
+
66
+ let client;
67
+ try {
68
+ ({ client } = await connect({ environment: options.environment }));
69
+ } catch {
70
+ // No cluster binding / unreadable kubeconfig — indeterminate, never a
71
+ // confident "not there".
72
+ return null;
73
+ }
74
+
75
+ const matched: K8sObject[] = [];
76
+ let anyRead = false;
77
+ for (const target of SWEEP_KINDS) {
78
+ try {
79
+ const items = await client.list(target, { labelSelector: selector });
80
+ anyRead = true;
81
+ matched.push(...items);
82
+ } catch {
83
+ // One kind's list failing (RBAC, an aggregated API down) proves nothing
84
+ // about the others; keep sweeping.
85
+ }
86
+ }
87
+ if (!anyRead) return null; // every read failed — indeterminate
88
+
89
+ if (matched.length === 0) return { stack: options.stack, present: false };
90
+
91
+ const verdicts = matched.map(objectHealthy).filter((v): v is boolean => v !== undefined);
92
+ const healthy = verdicts.length > 0 ? verdicts.every(Boolean) : true;
93
+ const ready = verdicts.filter(Boolean).length;
94
+ return {
95
+ stack: options.stack,
96
+ present: true,
97
+ status: verdicts.length > 0 ? `${ready}/${verdicts.length} workloads ready` : `${matched.length} objects present`,
98
+ healthy,
99
+ };
100
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,12 @@ export { k8sSerializer } from "./serializer";
4
4
  // Plugin
5
5
  export { k8sPlugin } from "./plugin";
6
6
 
7
+ // The capability plugin core's loader discovers on this package (#1495 piece 2)
8
+ // — the kubectl-apply leaf a component composes, the way aws contributes
9
+ // cfn-deploy.
10
+ export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./components/capability-plugin";
11
+ export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput } from "./components/kubectl-apply";
12
+
7
13
  // Default labels & annotations
8
14
  export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels";
9
15
  export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels";
package/src/plugin.ts CHANGED
@@ -631,6 +631,18 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
631
631
  return describeResources(options);
632
632
  },
633
633
 
634
+ /**
635
+ * Deploy-unit presence + health for `chant components status --live`
636
+ * (#1495 piece 3): a Kubernetes deploy unit is the label selector chant's
637
+ * own serializer stamps, so this reads back
638
+ * `app.kubernetes.io/managed-by=chant, chant.intentius.io/stack=<stack>`
639
+ * and rolls readiness up from the matching workloads' controllers.
640
+ */
641
+ async describeStackStatus(options) {
642
+ const { describeStackStatus } = await import("./describe-stack-status");
643
+ return describeStackStatus(options);
644
+ },
645
+
634
646
  async exportResources(options) {
635
647
  const { exportResources } = await import("./export-resources");
636
648
  return exportResources(options);
@@ -510,3 +510,105 @@ describe("k8sSerializer", () => {
510
510
  expect(result).not.toMatch(/^spec:\s*\n\s+rules:/m);
511
511
  });
512
512
  });
513
+
514
+ // ── Cross-resource references (#1493) ────────────────────────────────
515
+ //
516
+ // Kubernetes YAML has no deploy-time reference mechanism, so a reference has
517
+ // to be resolved before serialization or it means nothing. These used to
518
+ // serialize to the *logical* name, so a Deployment referencing its PVC emitted
519
+ // `claimName: pgClaim` and named a resource that does not exist — it applied
520
+ // cleanly and failed on the cluster.
521
+
522
+ function mockAttrRef(attribute: string, logicalName: string): any {
523
+ // Shape isAttrRefLike() checks for: parent object, string attribute, and
524
+ // _setLogicalName. Without the last one the walker treats it as a plain
525
+ // object and the test silently asserts nothing.
526
+ return {
527
+ attribute,
528
+ parent: { deref: () => undefined },
529
+ _setLogicalName: () => {},
530
+ getLogicalName: () => logicalName,
531
+ };
532
+ }
533
+
534
+ describe("cross-resource references resolve at build time (#1493)", () => {
535
+ test(".name resolves to the referenced resource's metadata.name", () => {
536
+ const claim = mockResource("K8s::Core::PersistentVolumeClaim", {
537
+ metadata: { name: "fountain-postgres", namespace: "fountain" },
538
+ spec: { accessModes: ["ReadWriteOnce"] },
539
+ });
540
+ const dep = mockResource("K8s::Apps::Deployment", {
541
+ metadata: { name: "fountain-postgres", namespace: "fountain" },
542
+ spec: {
543
+ template: {
544
+ spec: {
545
+ volumes: [
546
+ { name: "data", persistentVolumeClaim: { claimName: mockAttrRef("name", "pgClaim") } },
547
+ ],
548
+ },
549
+ },
550
+ },
551
+ });
552
+ const yaml = k8sSerializer.serialize(new Map([["pgClaim", claim], ["pgDeployment", dep]]));
553
+ expect(yaml).toContain("claimName: fountain-postgres");
554
+ expect(yaml).not.toContain("claimName: pgClaim");
555
+ });
556
+
557
+ // A resource that declares no name gets one derived from its logical name.
558
+ // A reference to it has to derive the SAME one or the two silently disagree.
559
+ test(".name matches the derived name when the target declares none", () => {
560
+ const claim = mockResource("K8s::Core::PersistentVolumeClaim", {
561
+ spec: { accessModes: ["ReadWriteOnce"] },
562
+ });
563
+ const dep = mockResource("K8s::Apps::Deployment", {
564
+ spec: { template: { spec: { volumes: [{ claimName: mockAttrRef("name", "pgClaim") }] } } },
565
+ });
566
+ const yaml = k8sSerializer.serialize(new Map([["pgClaim", claim], ["dep", dep]]));
567
+ expect(yaml).toContain("name: pg-claim");
568
+ expect(yaml).toContain("claimName: pg-claim");
569
+ });
570
+
571
+ test(".namespace resolves to the declared namespace", () => {
572
+ const svc = mockResource("K8s::Core::Service", {
573
+ metadata: { name: "fountain", namespace: "fountain" },
574
+ });
575
+ const dep = mockResource("K8s::Apps::Deployment", {
576
+ metadata: { name: "app" },
577
+ spec: { host: mockAttrRef("namespace", "service") },
578
+ });
579
+ const yaml = k8sSerializer.serialize(new Map([["service", svc], ["dep", dep]]));
580
+ expect(yaml).toContain("host: fountain");
581
+ });
582
+
583
+ // A UID is assigned by the API server at admission, so emitting any
584
+ // build-time string for it would be a fabrication.
585
+ test(".uid is refused rather than fabricated", () => {
586
+ const svc = mockResource("K8s::Core::Service", { metadata: { name: "fountain" } });
587
+ const dep = mockResource("K8s::Apps::Deployment", {
588
+ spec: { ownerRef: mockAttrRef("uid", "service") },
589
+ });
590
+ expect(() => k8sSerializer.serialize(new Map([["service", svc], ["dep", dep]]))).toThrow(
591
+ /assigned by the API server/,
592
+ );
593
+ });
594
+
595
+ test("an unresolvable namespace says what to set rather than emitting a wrong one", () => {
596
+ const svc = mockResource("K8s::Core::Service", { metadata: { name: "fountain" } });
597
+ const dep = mockResource("K8s::Apps::Deployment", {
598
+ spec: { ns: mockAttrRef("namespace", "service") },
599
+ });
600
+ expect(() => k8sSerializer.serialize(new Map([["service", svc], ["dep", dep]]))).toThrow(
601
+ /declares no metadata.namespace/,
602
+ );
603
+ });
604
+
605
+ test("an attribute k8s cannot express is refused with the reason", () => {
606
+ const svc = mockResource("K8s::Core::Service", { metadata: { name: "fountain" } });
607
+ const dep = mockResource("K8s::Apps::Deployment", {
608
+ spec: { x: mockAttrRef("clusterIP", "service") },
609
+ });
610
+ expect(() => k8sSerializer.serialize(new Map([["service", svc], ["dep", dep]]))).toThrow(
611
+ /resolves .name and .namespace/,
612
+ );
613
+ });
614
+ });
package/src/serializer.ts CHANGED
@@ -113,16 +113,82 @@ function deriveGVKFromType(entityType: string): { apiVersion: string; kind: stri
113
113
  return { apiVersion, kind };
114
114
  }
115
115
 
116
+ /**
117
+ * The name a resource gets when it does not declare one: the logical name,
118
+ * kebab-cased. Shared with the manifest builder below so a reference and the
119
+ * thing it references cannot derive different names (#1493).
120
+ */
121
+ function derivedName(logicalName: string): string {
122
+ return logicalName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
123
+ }
124
+
125
+ /**
126
+ * What a cross-resource reference resolves to on this substrate (#1493).
127
+ *
128
+ * Kubernetes YAML has no deploy-time reference mechanism — no `!Ref`, no
129
+ * `Fn::GetAtt`. Whatever a reference is going to mean has to be resolved here,
130
+ * before serialization, or it means nothing at all.
131
+ *
132
+ * This used to return the *logical* name for every attribute, so
133
+ * `claimName: pgClaim.name` emitted `claimName: pgClaim` and the manifest
134
+ * named a resource that does not exist. It applied cleanly and failed on the
135
+ * cluster, which is the failure this whole system exists to remove. The
136
+ * lexicon's own plugin comment already described the intended behaviour —
137
+ * "resolves to metadata.name" — so this brings the code to the documentation
138
+ * rather than the other way round.
139
+ *
140
+ * `uid` is refused rather than resolved: a UID is assigned by the API server
141
+ * at admission, so no build-time value exists and any string emitted here
142
+ * would be a fabrication.
143
+ */
144
+ function resolveK8sAttr(entity: Declarable | undefined, logicalName: string, attr: string): unknown {
145
+ if (attr === "uid") {
146
+ throw new Error(
147
+ `Cannot reference "${logicalName}.uid": a Kubernetes UID is assigned by the API server at ` +
148
+ `admission, so it has no build-time value. Reference .name, or carry the UID at runtime ` +
149
+ `with fieldRef: { fieldPath: "metadata.uid" }.`,
150
+ );
151
+ }
152
+
153
+ const metadata =
154
+ entity && isResourceDeclarable(entity) && typeof entity.props === "object" && entity.props !== null
155
+ ? ((entity.props as Record<string, unknown>).metadata as Record<string, unknown> | undefined)
156
+ : undefined;
157
+
158
+ if (attr === "name") {
159
+ // Same fallback the manifest builder applies, so a reference to a resource
160
+ // that declares no name still resolves to the name it will actually get.
161
+ const declared = metadata?.name;
162
+ return typeof declared === "string" && declared.length > 0 ? declared : derivedName(logicalName);
163
+ }
164
+
165
+ if (attr === "namespace") {
166
+ const ns = metadata?.namespace;
167
+ if (typeof ns === "string" && ns.length > 0) return ns;
168
+ throw new Error(
169
+ `Cannot reference "${logicalName}.namespace": it declares no metadata.namespace, and the ` +
170
+ `namespace a manifest lands in is decided at apply time (kubectl -n, or the context's ` +
171
+ `default). Set metadata.namespace on "${logicalName}" if the reference needs to be stable.`,
172
+ );
173
+ }
174
+
175
+ throw new Error(
176
+ `Cannot reference "${logicalName}.${attr}": the k8s lexicon resolves .name and .namespace at ` +
177
+ `build time, and Kubernetes YAML has no way to express any other attribute reference.`,
178
+ );
179
+ }
180
+
116
181
  /**
117
182
  * K8s visitor for the generic serializer walker.
118
183
  */
119
184
  function k8sVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
185
+ // name → entity, so an AttrRef can reach the resource it points at. The
186
+ // walker hands the visitor a logical name, not the Declarable.
187
+ const byName = new Map<string, Declarable>();
188
+ for (const [entity, name] of entityNames) byName.set(name, entity);
189
+
120
190
  return {
121
- attrRef: (name, attr) => {
122
- // For K8s, attribute references typically resolve to metadata.name
123
- if (attr === "name") return name;
124
- return name;
125
- },
191
+ attrRef: (name, attr) => resolveK8sAttr(byName.get(name), name, attr),
126
192
  resourceRef: (name) => name,
127
193
  propertyDeclarable: (entity, walk) => {
128
194
  if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
@@ -209,8 +275,7 @@ export const k8sSerializer: Serializer = {
209
275
  // Build metadata
210
276
  const metadata: Record<string, unknown> = props.metadata as Record<string, unknown> ?? {};
211
277
  if (!metadata.name) {
212
- // Use the logical name as the resource name (kebab-case)
213
- metadata.name = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
278
+ metadata.name = derivedName(name);
214
279
  }
215
280
 
216
281
  // Merge default labels