@intentius/chant-lexicon-k8s 0.26.0 → 0.28.0

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,62 @@
1
+ /**
2
+ * K8s environment → cluster binding — chant #1100.
3
+ *
4
+ * Every cloud lexicon binds an environment to a scope: AWS resolves `<env>`
5
+ * to a CloudFormation stack, Azure treats `<env>` as the resource group,
6
+ * Temporal looks up `temporal.profiles.<env>`. Before this, k8s bound
7
+ * nothing — `describeResources` shelled out to `kubectl get` with no
8
+ * `--context`, so `chant lifecycle diff prod --live` read whichever cluster
9
+ * `kubectl config current-context` happened to point at.
10
+ *
11
+ * Add this to `chant.config.ts` to pin an environment to a cluster:
12
+ *
13
+ * ```ts
14
+ * import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
15
+ *
16
+ * export default {
17
+ * lexicons: ["k8s"],
18
+ * k8s: {
19
+ * profiles: {
20
+ * prod: { context: "prod-eks" },
21
+ * staging: { context: "staging-eks" },
22
+ * },
23
+ * } satisfies K8sChantConfig,
24
+ * };
25
+ * ```
26
+ *
27
+ * `describeResources` (in `./describe-resources.ts`, and the GCP lexicon's
28
+ * Config Connector equivalent, which observes through the same kubectl path)
29
+ * resolves this via `@intentius/chant/kubectl-context`'s
30
+ * `resolveClusterTarget`:
31
+ *
32
+ * - A declared binding is passed explicitly as `kubectl ... --context <bound>`
33
+ * on every invocation, and checked against the ambient context first — a
34
+ * mismatch refuses loudly (naming the environment, the expected context,
35
+ * and the ambient one) instead of silently reading the wrong cluster.
36
+ * - No binding for the environment keeps today's behavior — the ambient
37
+ * context — but logs a visible note that nothing is pinned, so the
38
+ * fallback is never silent.
39
+ *
40
+ * `ChantConfig` uses `.passthrough()` in its Zod schema so the `k8s` key is
41
+ * accepted at runtime without core changes, exactly like `temporal.profiles`
42
+ * (see `lexicons/temporal/src/config.ts`).
43
+ *
44
+ * Deliberately out of scope here (see the issue for the full proposal):
45
+ * deriving the binding automatically from a declared `AWS::EKS::Cluster` /
46
+ * AKS / GKE cluster resource, and threading the resolved context into the
47
+ * Op write path (`op/activities/kubectl.ts`, `wait-for-ready.ts`) — both
48
+ * already accept an optional `context` a workflow author can pass, and can
49
+ * call `resolveClusterTarget` themselves to agree with what the read path
50
+ * resolved. This also keeps the binding available for #1073/#1074's future
51
+ * typed API client to inherit, rather than re-deriving it.
52
+ */
53
+ /** A single environment's cluster binding. */
54
+ export interface K8sClusterProfile {
55
+ /** kubectl context name this environment is bound to. */
56
+ context: string;
57
+ }
58
+ export interface K8sChantConfig {
59
+ /** Named environment → cluster bindings, keyed by environment name. */
60
+ profiles?: Record<string, K8sClusterProfile>;
61
+ }
62
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC9C"}
@@ -10,6 +10,14 @@
10
10
  * Resource-not-found is silent — `state diff --live` then reports it as
11
11
  * missing (declared, not in cloud). Unknown entity types are warn-skipped;
12
12
  * extending the KUBECTL_RESOURCE map covers more.
13
+ *
14
+ * Before touching any resource, the environment is resolved to a cluster
15
+ * identity (chant #1100) via `resolveClusterTarget` — see `./config.ts` for
16
+ * the `k8s.profiles.<env>.context` binding shape. A declared binding is
17
+ * passed explicitly as `--context` on every kubectl call below; an ambient
18
+ * context that disagrees with it aborts the whole describe with a loud
19
+ * error rather than silently reading the wrong cluster. No binding keeps
20
+ * today's behavior (ambient context), with a visible warning.
13
21
  */
14
22
  import type { ResourceMetadata } from "@intentius/chant/lexicon";
15
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoBnD,CAAC;AAsCF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAmD5C"}
1
+ {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAOjE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoBnD,CAAC;AAsCF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CA2D5C"}
package/dist/index.d.ts CHANGED
@@ -17,4 +17,5 @@ export type { PackageOptions, PackageResult } from "./codegen/package.js";
17
17
  export type { CRDSource, CRDSpec } from "./crd/types.js";
18
18
  export { parseCRD, parseCRDSpec } from "./crd/parser.js";
19
19
  export { loadCRDs, loadMultipleCRDs } from "./crd/loader.js";
20
+ export type { K8sChantConfig, K8sClusterProfile } from "./config.js";
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC5G,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAGrF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGxD,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAI/D,cAAc,mBAAmB,CAAC;AAGlC,OAAO,EACL,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EACzF,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,kBAAkB,EACxF,kBAAkB,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EACjH,aAAa,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,UAAU,EAAE,sBAAsB,EAC3H,UAAU,EAAE,kBAAkB,EAAE,sBAAsB,EACtD,UAAU,EAAE,MAAM,EAAE,UAAU,EAC9B,UAAU,EAAE,oBAAoB,EAAE,mBAAmB,EACrD,WAAW,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,qBAAqB,EAChF,iCAAiC,EACjC,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,GAC9E,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,kBAAkB,EACrG,sBAAsB,EAAE,uBAAuB,EAAE,eAAe,EAAE,gBAAgB,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,cAAc,EAAE,eAAe,EACtE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,EACtE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,gBAAgB,EAC1E,qBAAqB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,wBAAwB,EAChG,uBAAuB,EAAE,wBAAwB,EAAE,eAAe,EAAE,gBAAgB,EACpF,oBAAoB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,qBAAqB,EACxF,mBAAmB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,sBAAsB,EACxF,kBAAkB,EAAE,mBAAmB,EACvC,kBAAkB,EAAE,mBAAmB,EACvC,mCAAmC,EAAE,oCAAoC,EACzE,sBAAsB,EAAE,uBAAuB,EAC/C,0BAA0B,EAAE,2BAA2B,EACvD,eAAe,EAAE,gBAAgB,EACjC,2BAA2B,EAAE,4BAA4B,EACzD,eAAe,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,wBAAwB,EACpF,4BAA4B,EAAE,gCAAgC,EAAE,0BAA0B,EAC1F,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAC/F,WAAW,EAAE,YAAY,EACzB,eAAe,EAAE,gBAAgB,EACjC,eAAe,EAAE,cAAc,EAC/B,iBAAiB,EAAE,gBAAgB,EACnC,gBAAgB,EAAE,2BAA2B,EAAE,0BAA0B,EACzE,0BAA0B,EAAE,yBAAyB,EACrD,gBAAgB,EAAE,iBAAiB,EACnC,0BAA0B,EAAE,2BAA2B,EACvD,0BAA0B,EAAE,2BAA2B,EACvD,0BAA0B,EAAE,2BAA2B,EACvD,sCAAsC,EAAE,uCAAuC,EAC/E,sBAAsB,EAAE,uBAAuB,EAC/C,qBAAqB,EAAE,sBAAsB,EAC7C,wBAAwB,EAAE,yBAAyB,EACnD,wBAAwB,EAAE,yBAAyB,GACpD,MAAM,oBAAoB,CAAC;AAG5B,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC7G,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrI,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC5G,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAGrF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGxD,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAI/D,cAAc,mBAAmB,CAAC;AAGlC,OAAO,EACL,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EACzF,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,kBAAkB,EACxF,kBAAkB,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EACjH,aAAa,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,UAAU,EAAE,sBAAsB,EAC3H,UAAU,EAAE,kBAAkB,EAAE,sBAAsB,EACtD,UAAU,EAAE,MAAM,EAAE,UAAU,EAC9B,UAAU,EAAE,oBAAoB,EAAE,mBAAmB,EACrD,WAAW,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,qBAAqB,EAChF,iCAAiC,EACjC,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,GAC9E,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,kBAAkB,EACrG,sBAAsB,EAAE,uBAAuB,EAAE,eAAe,EAAE,gBAAgB,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,cAAc,EAAE,eAAe,EACtE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,EACtE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,gBAAgB,EAC1E,qBAAqB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,wBAAwB,EAChG,uBAAuB,EAAE,wBAAwB,EAAE,eAAe,EAAE,gBAAgB,EACpF,oBAAoB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,qBAAqB,EACxF,mBAAmB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,sBAAsB,EACxF,kBAAkB,EAAE,mBAAmB,EACvC,kBAAkB,EAAE,mBAAmB,EACvC,mCAAmC,EAAE,oCAAoC,EACzE,sBAAsB,EAAE,uBAAuB,EAC/C,0BAA0B,EAAE,2BAA2B,EACvD,eAAe,EAAE,gBAAgB,EACjC,2BAA2B,EAAE,4BAA4B,EACzD,eAAe,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,wBAAwB,EACpF,4BAA4B,EAAE,gCAAgC,EAAE,0BAA0B,EAC1F,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAC/F,WAAW,EAAE,YAAY,EACzB,eAAe,EAAE,gBAAgB,EACjC,eAAe,EAAE,cAAc,EAC/B,iBAAiB,EAAE,gBAAgB,EACnC,gBAAgB,EAAE,2BAA2B,EAAE,0BAA0B,EACzE,0BAA0B,EAAE,yBAAyB,EACrD,gBAAgB,EAAE,iBAAiB,EACnC,0BAA0B,EAAE,2BAA2B,EACvD,0BAA0B,EAAE,2BAA2B,EACvD,0BAA0B,EAAE,2BAA2B,EACvD,sCAAsC,EAAE,uCAAuC,EAC/E,sBAAsB,EAAE,uBAAuB,EAC/C,qBAAqB,EAAE,sBAAsB,EAC7C,wBAAwB,EAAE,yBAAyB,EACnD,wBAAwB,EAAE,yBAAyB,GACpD,MAAM,oBAAoB,CAAC;AAG5B,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC7G,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrI,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG1D,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "d40f4a07045e93834d4c246690f15d776f08f6591d0b8b20c230393948e188e8",
4
+ "manifest.json": "63b8e4b79b980ec7b6fb5e49b43686eeb7b024e6feab86d6d71169c65db10587",
5
5
  "meta.json": "6d112006daa7196eb28619ac5a3893b709dc2b845edbd537db7d7b471e8dc104",
6
6
  "types/index.d.ts": "7b873148146a5512c22baabfde6cfcc3248c24c2c7578f68a19544af5dc675f6",
7
7
  "rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
@@ -45,10 +45,10 @@
45
45
  "skills/chant-k8s-patterns.md": "683148e9f0f9da6879bd65d97c82cc29dbff897e31bc11b3c1de654cd6baeea3",
46
46
  "skills/chant-k8s-deployment-strategies.md": "c4cc93cba064230567baa755b5a3493dc69bfc6e74a62e1990d12039ffaaf5f8",
47
47
  "skills/chant-k8s-security.md": "f4250284000fde0d380192ba4cd73f2190471da2142e3bf77d848881552807f7",
48
- "skills/chant-k8s-eks.md": "c391217039b35d1c9d88214e2cb7b8c1166331d6ab26996f5f7d907027508ab9",
48
+ "skills/chant-k8s-eks.md": "9e953d6a2c39518c4b797a2bf54ff5b4a33530503e5b6d66e35921e8f952432f",
49
49
  "skills/chant-k8s-gke.md": "8938840bf9ef5ed58d6333fdd773b3dd54ecaf25a9df35e58f7f5c3355d4928f",
50
50
  "skills/chant-k8s-aks.md": "e18f0e2b055f72cd7a37deaf258d7027c2d4d3e286e8fd4975b27a1f981a3ad9",
51
51
  "skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5"
52
52
  },
53
- "composite": "9bf95c79612e8d3c9388f72fd161048857e42b351ce3040895c61e70dae95627"
53
+ "composite": "d6deea6f0e6f08944da12ce71f90784b9fce48659eee22cbf223ae331c7e1e22"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -19,4 +19,6 @@ export { waitForArgoSync, defaultArgoStatusFetcher, ArgoSyncFailedError } from "
19
19
  export type { WaitForArgoSyncArgs, ArgoAppStatus, ArgoStatusFetcher } from "./argo.js";
20
20
  export { waitForReady, defaultResourceFetcher, ReadinessFailedError, readinessFor, isReady, firstTerminal, DEFAULT_READINESS, READINESS_OVERRIDES, } from "./wait-for-ready.js";
21
21
  export type { WaitForReadyArgs, ResourceFetcher, ReadinessSpec, ReadinessMatch, ConditionMatch, PathMatch, } from "./wait-for-ready.js";
22
+ export { resolveClusterTarget, ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
23
+ export type { ResolvedClusterTarget, K8sClusterProfile, K8sConfigShape } from "@intentius/chant/kubectl-context";
22
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACvF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAEpF,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACvF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAEpF,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,kBAAkB,CAAC;AAW1B,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AACrG,YAAY,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC"}
@@ -1,6 +1,11 @@
1
1
  export interface KubectlApplyArgs {
2
2
  manifest: string;
3
- /** kubectl context name. Uses current context if omitted. */
3
+ /**
4
+ * kubectl context name. Uses the ambient context if omitted. To target the
5
+ * same cluster the read path (`describeResources`) resolved for an
6
+ * environment — chant #1100 — resolve it with `resolveClusterTarget` from
7
+ * `./index.ts` and pass `.context` through.
8
+ */
4
9
  context?: string;
5
10
  }
6
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9F"}
1
+ {"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9F"}
@@ -65,7 +65,12 @@ export interface WaitForReadyArgs {
65
65
  name: string;
66
66
  /** Namespace (omit for cluster-scoped). */
67
67
  namespace?: string;
68
- /** kubectl context. */
68
+ /**
69
+ * kubectl context. To target the same cluster the read path
70
+ * (`describeResources`) resolved for an environment — chant #1100 —
71
+ * resolve it with `resolveClusterTarget` from `./index.ts` and pass
72
+ * `.context` through.
73
+ */
69
74
  context?: string;
70
75
  /** API group, used to pick a readiness override when `spec` is not given. */
71
76
  group?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"wait-for-ready.d.ts","sourceRoot":"","sources":["../../../src/op/activities/wait-for-ready.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AAIH,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2DAA2D;AAC3D,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACnC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CAEhC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAG/B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAS7D,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAEnF;AAwCD,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAGlE;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,cAAc,GAAG,SAAS,CAE3F;AAID,6EAA6E;AAC7E,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,CAC5B,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAUtC,eAAO,MAAM,sBAAsB,EAAE,eAAiE,CAAC;AAEvG;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,eAAwC,GAChD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuBlC"}
1
+ {"version":3,"file":"wait-for-ready.d.ts","sourceRoot":"","sources":["../../../src/op/activities/wait-for-ready.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AAIH,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2DAA2D;AAC3D,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACnC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CAEhC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAG/B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAS7D,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAEnF;AAwCD,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAGlE;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,cAAc,GAAG,SAAS,CAE3F;AAID,6EAA6E;AAC7E,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,CAC5B,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAUtC,eAAO,MAAM,sBAAsB,EAAE,eAAiE,CAAC;AAEvG;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,eAAwC,GAChD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuBlC"}
@@ -127,13 +127,28 @@ For Pod Identity, no K8s-side composite is needed — configure the association
127
127
 
128
128
  ## Karpenter
129
129
 
130
- Karpenter replaces Cluster Autoscaler for node provisioning. Karpenter NodePool and EC2NodeClass are simple CRDs — use CRD import rather than composites:
130
+ Karpenter replaces Cluster Autoscaler for node provisioning. Karpenter's `NodePool` and `EC2NodeClass` are simple CRDs — add them to the k8s lexicon's CRD source list rather than importing per-project. There is no `chant import --url` flag; CRD-from-URL is lexicon-authoring-time codegen, done once in `lexicons/k8s/src/crd/crd-sources.ts`, not a per-project command:
131
+
132
+ ```typescript
133
+ // lexicons/k8s/src/crd/crd-sources.ts
134
+ const KARPENTER_VERSION = "v1.0.0"; // pin an operator release, not `main`
135
+ const KARPENTER_CRD_BASE = `https://raw.githubusercontent.com/aws/karpenter-provider-aws/${KARPENTER_VERSION}/pkg/apis/crds`;
136
+
137
+ export const CRD_SOURCES: CRDSource[] = [
138
+ // ...existing sources
139
+ { type: "url", url: `${KARPENTER_CRD_BASE}/karpenter.sh_nodepools.yaml` },
140
+ { type: "url", url: `${KARPENTER_CRD_BASE}/karpenter.k8s.aws_ec2nodeclasses.yaml` },
141
+ ];
142
+ ```
143
+
144
+ Then regenerate the lexicon so the new kinds are typed:
131
145
 
132
146
  ```bash
133
- # Import Karpenter CRDs into your chant project
134
- chant import --url https://raw.githubusercontent.com/aws/karpenter/main/pkg/apis/crds/karpenter.sh_nodepools.yaml
147
+ npm run generate -w @intentius/chant-lexicon-k8s
135
148
  ```
136
149
 
150
+ See [Add a Third-Party CRD](/chant/lexicon-authoring/crd-sources/) for the full workflow (namespace mapping, verifying the generated type, adding rules).
151
+
137
152
  ## Fargate Considerations
138
153
 
139
154
  When running on EKS Fargate:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -75,7 +75,7 @@
75
75
  "typescript": "^5.9.3"
76
76
  },
77
77
  "peerDependencies": {
78
- "@intentius/chant": "^0.26.0",
78
+ "@intentius/chant": "^0.28.0",
79
79
  "typescript": "^5.9.3"
80
80
  }
81
81
  }
package/src/config.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * K8s environment → cluster binding — chant #1100.
3
+ *
4
+ * Every cloud lexicon binds an environment to a scope: AWS resolves `<env>`
5
+ * to a CloudFormation stack, Azure treats `<env>` as the resource group,
6
+ * Temporal looks up `temporal.profiles.<env>`. Before this, k8s bound
7
+ * nothing — `describeResources` shelled out to `kubectl get` with no
8
+ * `--context`, so `chant lifecycle diff prod --live` read whichever cluster
9
+ * `kubectl config current-context` happened to point at.
10
+ *
11
+ * Add this to `chant.config.ts` to pin an environment to a cluster:
12
+ *
13
+ * ```ts
14
+ * import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
15
+ *
16
+ * export default {
17
+ * lexicons: ["k8s"],
18
+ * k8s: {
19
+ * profiles: {
20
+ * prod: { context: "prod-eks" },
21
+ * staging: { context: "staging-eks" },
22
+ * },
23
+ * } satisfies K8sChantConfig,
24
+ * };
25
+ * ```
26
+ *
27
+ * `describeResources` (in `./describe-resources.ts`, and the GCP lexicon's
28
+ * Config Connector equivalent, which observes through the same kubectl path)
29
+ * resolves this via `@intentius/chant/kubectl-context`'s
30
+ * `resolveClusterTarget`:
31
+ *
32
+ * - A declared binding is passed explicitly as `kubectl ... --context <bound>`
33
+ * on every invocation, and checked against the ambient context first — a
34
+ * mismatch refuses loudly (naming the environment, the expected context,
35
+ * and the ambient one) instead of silently reading the wrong cluster.
36
+ * - No binding for the environment keeps today's behavior — the ambient
37
+ * context — but logs a visible note that nothing is pinned, so the
38
+ * fallback is never silent.
39
+ *
40
+ * `ChantConfig` uses `.passthrough()` in its Zod schema so the `k8s` key is
41
+ * accepted at runtime without core changes, exactly like `temporal.profiles`
42
+ * (see `lexicons/temporal/src/config.ts`).
43
+ *
44
+ * Deliberately out of scope here (see the issue for the full proposal):
45
+ * deriving the binding automatically from a declared `AWS::EKS::Cluster` /
46
+ * AKS / GKE cluster resource, and threading the resolved context into the
47
+ * Op write path (`op/activities/kubectl.ts`, `wait-for-ready.ts`) — both
48
+ * already accept an optional `context` a workflow author can pass, and can
49
+ * call `resolveClusterTarget` themselves to agree with what the read path
50
+ * resolved. This also keeps the binding available for #1073/#1074's future
51
+ * typed API client to inherit, rather than re-deriving it.
52
+ */
53
+
54
+ /** A single environment's cluster binding. */
55
+ export interface K8sClusterProfile {
56
+ /** kubectl context name this environment is bound to. */
57
+ context: string;
58
+ }
59
+
60
+ export interface K8sChantConfig {
61
+ /** Named environment → cluster bindings, keyed by environment name. */
62
+ profiles?: Record<string, K8sClusterProfile>;
63
+ }
@@ -11,6 +11,11 @@ vi.mock("node:child_process", async () => {
11
11
  } };
12
12
  });
13
13
 
14
+ const loadChantConfigMock = vi.fn();
15
+ vi.mock("@intentius/chant/config", () => ({
16
+ loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
17
+ }));
18
+
14
19
  const { describeResources } = await import("./describe-resources");
15
20
 
16
21
  function makeEntities(records: Array<{ name: string; entityType: string; props: Record<string, unknown> }>) {
@@ -20,6 +25,10 @@ function makeEntities(records: Array<{ name: string; entityType: string; props:
20
25
  describe("k8s describeResources", () => {
21
26
  beforeEach(() => {
22
27
  execMock.mockReset();
28
+ loadChantConfigMock.mockReset();
29
+ // No binding declared by default — matches every test below except the
30
+ // dedicated cluster-binding tests, which override this per case.
31
+ loadChantConfigMock.mockResolvedValue({ config: {} });
23
32
  });
24
33
 
25
34
  test("queries kubectl for each declared K8s entity and maps response to ResourceMetadata", async () => {
@@ -86,7 +95,8 @@ describe("k8s describeResources", () => {
86
95
 
87
96
  expect(result).toEqual({});
88
97
  expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("kubectl mapping"));
89
- expect(warnSpy.mock.calls[0][0]).toContain("K8s::Custom::CRD::SomeOperator");
98
+ const mappingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("kubectl mapping"));
99
+ expect(mappingWarning?.[0]).toContain("K8s::Custom::CRD::SomeOperator");
90
100
  warnSpy.mockRestore();
91
101
  });
92
102
 
@@ -159,4 +169,72 @@ describe("k8s describeResources", () => {
159
169
  expect(result).toEqual({});
160
170
  expect(execMock).not.toHaveBeenCalled();
161
171
  });
172
+
173
+ // chant #1100 — environment→cluster binding: bound-and-matching,
174
+ // bound-and-mismatched (loud refusal), and unbound (unchanged) paths.
175
+ describe("cluster binding (chant #1100)", () => {
176
+ function deploymentEntities() {
177
+ return makeEntities([
178
+ { name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
179
+ ]);
180
+ }
181
+
182
+ const deploymentStdout = JSON.stringify({
183
+ metadata: { name: "web", namespace: "prod", uid: "uid-1", creationTimestamp: "2026-05-01T00:00:00Z" },
184
+ status: { readyReplicas: 1, replicas: 1 },
185
+ });
186
+
187
+ test("bound and ambient context matches: observes explicitly via --context", async () => {
188
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
189
+ let receivedCmd = "";
190
+ execMock.mockImplementation((cmd: string) => {
191
+ if (cmd.includes("current-context")) return { stdout: "prod-eks\n", stderr: "" };
192
+ receivedCmd = cmd;
193
+ return { stdout: deploymentStdout, stderr: "" };
194
+ });
195
+
196
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() });
197
+
198
+ expect(receivedCmd).toContain("--context prod-eks");
199
+ expect(result["web"]).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-1", status: "READY" });
200
+ });
201
+
202
+ test("bound and ambient context mismatches: refuses loudly instead of observing the wrong cluster", async () => {
203
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
204
+ execMock.mockImplementation((cmd: string) => {
205
+ if (cmd.includes("current-context")) return { stdout: "staging-eks\n", stderr: "" };
206
+ throw new Error(`unexpected cmd (should have refused before any kubectl get): ${cmd}`);
207
+ });
208
+
209
+ await expect(
210
+ describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() }),
211
+ ).rejects.toThrow(/environment "prod".*"prod-eks".*"staging-eks"/s);
212
+
213
+ // Only the ambient-context probe ran — no per-entity kubectl get was attempted.
214
+ expect(execMock).toHaveBeenCalledTimes(1);
215
+ });
216
+
217
+ test("unbound: ambient context is used unchanged, but the fallback is visible (not silent)", async () => {
218
+ loadChantConfigMock.mockResolvedValue({ config: {} });
219
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
220
+ let receivedCmd = "";
221
+ execMock.mockImplementation((cmd: string) => {
222
+ receivedCmd = cmd;
223
+ return { stdout: deploymentStdout, stderr: "" };
224
+ });
225
+
226
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() });
227
+
228
+ // Unchanged: no ambient-context probe, no --context flag, same result shape as today.
229
+ expect(receivedCmd).not.toContain("--context");
230
+ expect(receivedCmd).not.toContain("current-context");
231
+ expect(result["web"]).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-1", status: "READY" });
232
+
233
+ // Visible, not silent: a warning names the environment and the missing binding.
234
+ const bindingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("no cluster binding"));
235
+ expect(bindingWarning?.[0]).toContain('environment "prod"');
236
+ expect(bindingWarning?.[0]).toContain("k8s.profiles.prod.context");
237
+ warnSpy.mockRestore();
238
+ });
239
+ });
162
240
  });
@@ -10,12 +10,22 @@
10
10
  * Resource-not-found is silent — `state diff --live` then reports it as
11
11
  * missing (declared, not in cloud). Unknown entity types are warn-skipped;
12
12
  * extending the KUBECTL_RESOURCE map covers more.
13
+ *
14
+ * Before touching any resource, the environment is resolved to a cluster
15
+ * identity (chant #1100) via `resolveClusterTarget` — see `./config.ts` for
16
+ * the `k8s.profiles.<env>.context` binding shape. A declared binding is
17
+ * passed explicitly as `--context` on every kubectl call below; an ambient
18
+ * context that disagrees with it aborts the whole describe with a loud
19
+ * error rather than silently reading the wrong cluster. No binding keeps
20
+ * today's behavior (ambient context), with a visible warning.
13
21
  */
14
22
 
15
23
  import { exec } from "node:child_process";
16
24
  import { promisify } from "node:util";
17
25
  import type { ResourceMetadata } from "@intentius/chant/lexicon";
18
26
  import { hasOwnershipMarker, classifyOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
27
+ import { loadChantConfig } from "@intentius/chant/config";
28
+ import { resolveClusterTarget } from "@intentius/chant/kubectl-context";
19
29
 
20
30
  const execAsync = promisify(exec);
21
31
 
@@ -91,6 +101,14 @@ export async function describeResources(options: {
91
101
  const result: Record<string, ResourceMetadata> = {};
92
102
  const skippedTypes = new Set<string>();
93
103
 
104
+ // Resolve the cluster identity for this environment before touching any
105
+ // resource — a declared-but-mismatched binding throws here, aborting the
106
+ // whole describe rather than letting a per-entity try/catch below absorb
107
+ // it as an ordinary "not found".
108
+ const { config } = await loadChantConfig(process.cwd());
109
+ const target = await resolveClusterTarget(config as Record<string, unknown>, options.environment, "k8s");
110
+ const ctxArg = target.context ? ["--context", target.context] : [];
111
+
94
112
  for (const [entityName, { entityType, props }] of options.entities) {
95
113
  const kubectlResource = KUBECTL_RESOURCE[entityType];
96
114
  if (!kubectlResource) {
@@ -103,7 +121,7 @@ export async function describeResources(options: {
103
121
  if (!name) continue;
104
122
 
105
123
  const nsArg = metadata.namespace ? ["-n", metadata.namespace] : [];
106
- const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, "-o", "json"].join(" ");
124
+ const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, ...ctxArg, "-o", "json"].join(" ");
107
125
 
108
126
  try {
109
127
  const { stdout } = await execAsync(cmd);
package/src/index.ts CHANGED
@@ -85,3 +85,6 @@ export type { PackageOptions, PackageResult } from "./codegen/package";
85
85
  export type { CRDSource, CRDSpec } from "./crd/types";
86
86
  export { parseCRD, parseCRDSpec } from "./crd/parser";
87
87
  export { loadCRDs, loadMultipleCRDs } from "./crd/loader";
88
+
89
+ // Environment → cluster binding config shape (chant #1100)
90
+ export type { K8sChantConfig, K8sClusterProfile } from "./config";
@@ -38,3 +38,15 @@ export type {
38
38
  ConditionMatch,
39
39
  PathMatch,
40
40
  } from "./wait-for-ready";
41
+
42
+ // Environment→cluster binding (chant #1100) — the same resolver
43
+ // `describeResources` (the read path) uses. `kubectlApply` and `waitForReady`
44
+ // above already accept an optional `context`; a workflow author who wants the
45
+ // write path to target the environment's bound cluster (rather than an
46
+ // ambient one, closing the read/write split) resolves it here and passes the
47
+ // result through, e.g. `kubectlApply({ manifest, context: (await
48
+ // resolveClusterTarget(config, environment, "k8s")).context })`. Re-exported
49
+ // from core rather than duplicated so both paths agree on one source of
50
+ // truth — see `lexicons/k8s/src/config.ts` for the config shape.
51
+ export { resolveClusterTarget, ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
52
+ export type { ResolvedClusterTarget, K8sClusterProfile, K8sConfigShape } from "@intentius/chant/kubectl-context";
@@ -6,7 +6,12 @@ const execAsync = promisify(exec);
6
6
 
7
7
  export interface KubectlApplyArgs {
8
8
  manifest: string;
9
- /** kubectl context name. Uses current context if omitted. */
9
+ /**
10
+ * kubectl context name. Uses the ambient context if omitted. To target the
11
+ * same cluster the read path (`describeResources`) resolved for an
12
+ * environment — chant #1100 — resolve it with `resolveClusterTarget` from
13
+ * `./index.ts` and pass `.context` through.
14
+ */
10
15
  context?: string;
11
16
  }
12
17
 
@@ -147,7 +147,12 @@ export interface WaitForReadyArgs {
147
147
  name: string;
148
148
  /** Namespace (omit for cluster-scoped). */
149
149
  namespace?: string;
150
- /** kubectl context. */
150
+ /**
151
+ * kubectl context. To target the same cluster the read path
152
+ * (`describeResources`) resolved for an environment — chant #1100 —
153
+ * resolve it with `resolveClusterTarget` from `./index.ts` and pass
154
+ * `.context` through.
155
+ */
151
156
  context?: string;
152
157
  /** API group, used to pick a readiness override when `spec` is not given. */
153
158
  group?: string;
@@ -127,13 +127,28 @@ For Pod Identity, no K8s-side composite is needed — configure the association
127
127
 
128
128
  ## Karpenter
129
129
 
130
- Karpenter replaces Cluster Autoscaler for node provisioning. Karpenter NodePool and EC2NodeClass are simple CRDs — use CRD import rather than composites:
130
+ Karpenter replaces Cluster Autoscaler for node provisioning. Karpenter's `NodePool` and `EC2NodeClass` are simple CRDs — add them to the k8s lexicon's CRD source list rather than importing per-project. There is no `chant import --url` flag; CRD-from-URL is lexicon-authoring-time codegen, done once in `lexicons/k8s/src/crd/crd-sources.ts`, not a per-project command:
131
+
132
+ ```typescript
133
+ // lexicons/k8s/src/crd/crd-sources.ts
134
+ const KARPENTER_VERSION = "v1.0.0"; // pin an operator release, not `main`
135
+ const KARPENTER_CRD_BASE = `https://raw.githubusercontent.com/aws/karpenter-provider-aws/${KARPENTER_VERSION}/pkg/apis/crds`;
136
+
137
+ export const CRD_SOURCES: CRDSource[] = [
138
+ // ...existing sources
139
+ { type: "url", url: `${KARPENTER_CRD_BASE}/karpenter.sh_nodepools.yaml` },
140
+ { type: "url", url: `${KARPENTER_CRD_BASE}/karpenter.k8s.aws_ec2nodeclasses.yaml` },
141
+ ];
142
+ ```
143
+
144
+ Then regenerate the lexicon so the new kinds are typed:
131
145
 
132
146
  ```bash
133
- # Import Karpenter CRDs into your chant project
134
- chant import --url https://raw.githubusercontent.com/aws/karpenter/main/pkg/apis/crds/karpenter.sh_nodepools.yaml
147
+ npm run generate -w @intentius/chant-lexicon-k8s
135
148
  ```
136
149
 
150
+ See [Add a Third-Party CRD](/chant/lexicon-authoring/crd-sources/) for the full workflow (namespace mapping, verifying the generated type, adding rules).
151
+
137
152
  ## Fargate Considerations
138
153
 
139
154
  When running on EKS Fargate: