@intentius/chant-lexicon-k8s 0.27.0 → 0.29.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"}
@@ -7,11 +7,24 @@
7
7
  * name (using the props.metadata.name + props.metadata.namespace from #39's
8
8
  * entity-prop pass-through).
9
9
  *
10
- * Resource-not-found is silent `state diff --live` then reports it as
11
- * missing (declared, not in cloud). Unknown entity types are warn-skipped;
12
- * extending the KUBECTL_RESOURCE map covers more.
10
+ * The observation tri-state (#1089) is what the return value carries. A genuine
11
+ * `NotFound` from the API server is an absence, and only that becomes a `create`
12
+ * downstream. An entity type with no entry in `KUBECTL_RESOURCE` every CRD —
13
+ * was never looked at, and comes back `unsupported-kind`: it may well be running
14
+ * in the cluster, and proposing to create it would be a guess. Auth failures and
15
+ * unreachable API servers come back `no-credentials` / `no-binding` for the same
16
+ * reason. Extending the KUBECTL_RESOURCE map converts unsupported-kind holes into
17
+ * real reads; until then they are holes chant admits to.
18
+ *
19
+ * Before touching any resource, the environment is resolved to a cluster
20
+ * identity (chant #1100) via `resolveClusterTarget` — see `./config.ts` for
21
+ * the `k8s.profiles.<env>.context` binding shape. A declared binding is
22
+ * passed explicitly as `--context` on every kubectl call below; an ambient
23
+ * context that disagrees with it aborts the whole describe with a loud
24
+ * error rather than silently reading the wrong cluster. No binding keeps
25
+ * today's behavior (ambient context), with a visible warning.
13
26
  */
14
- import type { ResourceMetadata } from "@intentius/chant/lexicon";
27
+ import type { ObservationResult } from "@intentius/chant/lexicon";
15
28
  /**
16
29
  * Map chant entity types to `kubectl get` resource names. Add entries here
17
30
  * as new types are needed.
@@ -26,5 +39,5 @@ export declare function describeResources(options: {
26
39
  props: Record<string, unknown>;
27
40
  }>;
28
41
  owned?: boolean;
29
- }): Promise<Record<string, ResourceMetadata>>;
42
+ }): Promise<ObservationResult>;
30
43
  //# sourceMappingURL=describe-resources.d.ts.map
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAQtG;;;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,iBAAiB,CAAC,CAyF7B"}
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":"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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": "8a7018cadb51107c42bba4cc99824ae8c0cbf77ffa904361ce90ef9cadc7cc38",
4
+ "manifest.json": "bea5120e32340b17c1c92685893306ca70028b56c4a2bf79da7ad99917a99b4f",
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": "9ed36c6a5b40d86a9d06183827b6efdcf082deff14a890cc0f1281ce0d464d84"
53
+ "composite": "52fb1f56f334cf400a0544278a42f1d85d52aa9f49f55b4d845b9844e2e42aa4"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.27.0",
3
+ "version": "0.29.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.27.0",
3
+ "version": "0.29.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.27.0",
78
+ "@intentius/chant": "^0.29.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 () => {
@@ -51,13 +60,13 @@ describe("k8s describeResources", () => {
51
60
 
52
61
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web", "webSvc"], entities });
53
62
 
54
- expect(result["web"]).toMatchObject({
63
+ expect(result.resources["web"]).toMatchObject({
55
64
  type: "K8s::Apps::Deployment",
56
65
  physicalId: "uid-1",
57
66
  status: "READY",
58
67
  attributes: expect.objectContaining({ namespace: "prod", labels: { app: "web" } }),
59
68
  });
60
- expect(result["webSvc"]).toMatchObject({
69
+ expect(result.resources["webSvc"]).toMatchObject({
61
70
  type: "K8s::Core::Service",
62
71
  physicalId: "uid-2",
63
72
  status: "PRESENT",
@@ -73,10 +82,12 @@ describe("k8s describeResources", () => {
73
82
 
74
83
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["missing"], entities });
75
84
 
76
- expect(result).toEqual({});
85
+ // A real NotFound is an absence: neither present nor unobserved.
86
+ expect(result.resources).toEqual({});
87
+ expect(result.unobserved ?? {}).toEqual({});
77
88
  });
78
89
 
79
- test("entity types without kubectl mapping are warn-skipped", async () => {
90
+ test("entity types without kubectl mapping are reported unobserved, not absent (#1089)", async () => {
80
91
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
81
92
  const entities = makeEntities([
82
93
  { name: "exotic", entityType: "K8s::Custom::CRD::SomeOperator", props: { metadata: { name: "x" } } },
@@ -84,12 +95,69 @@ describe("k8s describeResources", () => {
84
95
 
85
96
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["exotic"], entities });
86
97
 
87
- expect(result).toEqual({});
88
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("kubectl mapping"));
89
- expect(warnSpy.mock.calls[0][0]).toContain("K8s::Custom::CRD::SomeOperator");
98
+ expect(result.resources).toEqual({});
99
+ expect(result.unobserved?.exotic).toMatchObject({
100
+ type: "K8s::Custom::CRD::SomeOperator",
101
+ reason: "unsupported-kind",
102
+ });
103
+ expect(execMock).not.toHaveBeenCalled();
104
+ const mappingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("kubectl mapping"));
105
+ expect(mappingWarning?.[0]).toContain("K8s::Custom::CRD::SomeOperator");
90
106
  warnSpy.mockRestore();
91
107
  });
92
108
 
109
+ // #1089 — a kubectl failure that is not a NotFound proves nothing about the
110
+ // object's existence, and must not reach the change set as an absence.
111
+ test.each([
112
+ ["error: You must be logged in to the server (Unauthorized)", "no-credentials"],
113
+ ["The connection to the server 127.0.0.1:6443 was refused - did you specify the right host or port?", "no-binding"],
114
+ ["error: unexpected EOF while parsing", "read-failed"],
115
+ ])("kubectl failure %j is reported unobserved (%s)", async (stderr, reason) => {
116
+ execMock.mockImplementation(() => { throw Object.assign(new Error("kubectl failed"), { stderr }); });
117
+ const entities = makeEntities([
118
+ { name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
119
+ ]);
120
+
121
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities });
122
+
123
+ expect(result.resources).toEqual({});
124
+ expect(result.unobserved?.web?.reason).toBe(reason);
125
+ });
126
+
127
+ test("a CRD kind the server does not serve is a real absence (nothing of that kind can exist)", async () => {
128
+ execMock.mockImplementation(() => {
129
+ throw Object.assign(new Error("kubectl failed"), {
130
+ stderr: 'error: the server doesn\'t have a resource type "widgets"',
131
+ });
132
+ });
133
+ const entities = makeEntities([
134
+ { name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
135
+ ]);
136
+
137
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities });
138
+
139
+ expect(result.resources).toEqual({});
140
+ expect(result.unobserved ?? {}).toEqual({});
141
+ });
142
+
143
+ test("--owned withholds an unmarked live object as `filtered`, never as absent (#1089)", async () => {
144
+ execMock.mockResolvedValue({
145
+ stdout: JSON.stringify({
146
+ metadata: { name: "web", namespace: "prod", uid: "uid-1", creationTimestamp: "t", labels: { app: "web" } },
147
+ status: { readyReplicas: 1, replicas: 1 },
148
+ }),
149
+ stderr: "",
150
+ });
151
+ const entities = makeEntities([
152
+ { name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
153
+ ]);
154
+
155
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities, owned: true });
156
+
157
+ expect(result.resources).toEqual({});
158
+ expect(result.unobserved?.web?.reason).toBe("filtered");
159
+ });
160
+
93
161
  test("Deployment with replicas != readyReplicas reports PROGRESSING", async () => {
94
162
  execMock.mockResolvedValue({
95
163
  stdout: JSON.stringify({
@@ -105,7 +173,7 @@ describe("k8s describeResources", () => {
105
173
 
106
174
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities });
107
175
 
108
- expect(result["web"].status).toBe("PROGRESSING(1/3)");
176
+ expect(result.resources["web"].status).toBe("PROGRESSING(1/3)");
109
177
  });
110
178
 
111
179
  test("Pod uses status.phase as the status", async () => {
@@ -123,7 +191,7 @@ describe("k8s describeResources", () => {
123
191
 
124
192
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["p"], entities });
125
193
 
126
- expect(result["p"].status).toBe("Running");
194
+ expect(result.resources["p"].status).toBe("Running");
127
195
  });
128
196
 
129
197
  test("namespace-less resource omits the -n flag", async () => {
@@ -149,14 +217,83 @@ describe("k8s describeResources", () => {
149
217
  expect(receivedCmd).toContain("namespace mynamespace");
150
218
  });
151
219
 
152
- test("entity without metadata.name is silently skipped", async () => {
220
+ test("entity without metadata.name is unobserved — nothing was queried", async () => {
153
221
  const entities = makeEntities([
154
222
  { name: "broken", entityType: "K8s::Apps::Deployment", props: {} },
155
223
  ]);
156
224
 
157
225
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["broken"], entities });
158
226
 
159
- expect(result).toEqual({});
227
+ expect(result.resources).toEqual({});
228
+ expect(result.unobserved?.broken?.reason).toBe("read-failed");
160
229
  expect(execMock).not.toHaveBeenCalled();
161
230
  });
231
+
232
+ // chant #1100 — environment→cluster binding: bound-and-matching,
233
+ // bound-and-mismatched (loud refusal), and unbound (unchanged) paths.
234
+ describe("cluster binding (chant #1100)", () => {
235
+ function deploymentEntities() {
236
+ return makeEntities([
237
+ { name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
238
+ ]);
239
+ }
240
+
241
+ const deploymentStdout = JSON.stringify({
242
+ metadata: { name: "web", namespace: "prod", uid: "uid-1", creationTimestamp: "2026-05-01T00:00:00Z" },
243
+ status: { readyReplicas: 1, replicas: 1 },
244
+ });
245
+
246
+ test("bound and ambient context matches: observes explicitly via --context", async () => {
247
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
248
+ let receivedCmd = "";
249
+ execMock.mockImplementation((cmd: string) => {
250
+ if (cmd.includes("current-context")) return { stdout: "prod-eks\n", stderr: "" };
251
+ receivedCmd = cmd;
252
+ return { stdout: deploymentStdout, stderr: "" };
253
+ });
254
+
255
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() });
256
+
257
+ expect(receivedCmd).toContain("--context prod-eks");
258
+ expect(result.resources["web"]).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-1", status: "READY" });
259
+ });
260
+
261
+ test("bound and ambient context mismatches: refuses loudly instead of observing the wrong cluster", async () => {
262
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
263
+ execMock.mockImplementation((cmd: string) => {
264
+ if (cmd.includes("current-context")) return { stdout: "staging-eks\n", stderr: "" };
265
+ throw new Error(`unexpected cmd (should have refused before any kubectl get): ${cmd}`);
266
+ });
267
+
268
+ await expect(
269
+ describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() }),
270
+ ).rejects.toThrow(/environment "prod".*"prod-eks".*"staging-eks"/s);
271
+
272
+ // Only the ambient-context probe ran — no per-entity kubectl get was attempted.
273
+ expect(execMock).toHaveBeenCalledTimes(1);
274
+ });
275
+
276
+ test("unbound: ambient context is used unchanged, but the fallback is visible (not silent)", async () => {
277
+ loadChantConfigMock.mockResolvedValue({ config: {} });
278
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
279
+ let receivedCmd = "";
280
+ execMock.mockImplementation((cmd: string) => {
281
+ receivedCmd = cmd;
282
+ return { stdout: deploymentStdout, stderr: "" };
283
+ });
284
+
285
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["web"], entities: deploymentEntities() });
286
+
287
+ // Unchanged: no ambient-context probe, no --context flag, same result shape as today.
288
+ expect(receivedCmd).not.toContain("--context");
289
+ expect(receivedCmd).not.toContain("current-context");
290
+ expect(result.resources["web"]).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-1", status: "READY" });
291
+
292
+ // Visible, not silent: a warning names the environment and the missing binding.
293
+ const bindingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("no cluster binding"));
294
+ expect(bindingWarning?.[0]).toContain('environment "prod"');
295
+ expect(bindingWarning?.[0]).toContain("k8s.profiles.prod.context");
296
+ warnSpy.mockRestore();
297
+ });
298
+ });
162
299
  });
@@ -7,15 +7,31 @@
7
7
  * name (using the props.metadata.name + props.metadata.namespace from #39's
8
8
  * entity-prop pass-through).
9
9
  *
10
- * Resource-not-found is silent `state diff --live` then reports it as
11
- * missing (declared, not in cloud). Unknown entity types are warn-skipped;
12
- * extending the KUBECTL_RESOURCE map covers more.
10
+ * The observation tri-state (#1089) is what the return value carries. A genuine
11
+ * `NotFound` from the API server is an absence, and only that becomes a `create`
12
+ * downstream. An entity type with no entry in `KUBECTL_RESOURCE` every CRD —
13
+ * was never looked at, and comes back `unsupported-kind`: it may well be running
14
+ * in the cluster, and proposing to create it would be a guess. Auth failures and
15
+ * unreachable API servers come back `no-credentials` / `no-binding` for the same
16
+ * reason. Extending the KUBECTL_RESOURCE map converts unsupported-kind holes into
17
+ * real reads; until then they are holes chant admits to.
18
+ *
19
+ * Before touching any resource, the environment is resolved to a cluster
20
+ * identity (chant #1100) via `resolveClusterTarget` — see `./config.ts` for
21
+ * the `k8s.profiles.<env>.context` binding shape. A declared binding is
22
+ * passed explicitly as `--context` on every kubectl call below; an ambient
23
+ * context that disagrees with it aborts the whole describe with a loud
24
+ * error rather than silently reading the wrong cluster. No binding keeps
25
+ * today's behavior (ambient context), with a visible warning.
13
26
  */
14
27
 
15
28
  import { exec } from "node:child_process";
16
29
  import { promisify } from "node:util";
17
- import type { ResourceMetadata } from "@intentius/chant/lexicon";
30
+ import type { ObservationResult, ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
31
+ import { observation } from "@intentius/chant/observation";
18
32
  import { hasOwnershipMarker, classifyOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
33
+ import { loadChantConfig } from "@intentius/chant/config";
34
+ import { resolveClusterTarget, classifyKubectlFailure } from "@intentius/chant/kubectl-context";
19
35
 
20
36
  const execAsync = promisify(exec);
21
37
 
@@ -87,29 +103,62 @@ export async function describeResources(options: {
87
103
  entityNames: string[];
88
104
  entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
89
105
  owned?: boolean;
90
- }): Promise<Record<string, ResourceMetadata>> {
106
+ }): Promise<ObservationResult> {
91
107
  const result: Record<string, ResourceMetadata> = {};
108
+ const unobserved: Record<string, UnobservedEntity> = {};
92
109
  const skippedTypes = new Set<string>();
93
110
 
111
+ // Resolve the cluster identity for this environment before touching any
112
+ // resource — a declared-but-mismatched binding throws here, aborting the
113
+ // whole describe rather than letting a per-entity try/catch below absorb
114
+ // it as an ordinary "not found".
115
+ const { config } = await loadChantConfig(process.cwd());
116
+ const target = await resolveClusterTarget(config as Record<string, unknown>, options.environment, "k8s");
117
+ const ctxArg = target.context ? ["--context", target.context] : [];
118
+
94
119
  for (const [entityName, { entityType, props }] of options.entities) {
95
120
  const kubectlResource = KUBECTL_RESOURCE[entityType];
96
121
  if (!kubectlResource) {
122
+ // No reader for this kind (every CRD). The object may exist; chant has no
123
+ // way to ask. Reporting it as unobserved is what stops `lifecycle plan`
124
+ // proposing to create a CRD that is already in the cluster (#1089).
97
125
  skippedTypes.add(entityType);
126
+ unobserved[entityName] = {
127
+ type: entityType,
128
+ reason: "unsupported-kind",
129
+ detail: `no kubectl mapping for ${entityType} — extend KUBECTL_RESOURCE to observe it`,
130
+ };
98
131
  continue;
99
132
  }
100
133
 
101
134
  const metadata = props.metadata as { name?: string; namespace?: string } | undefined;
102
135
  const name = metadata?.name;
103
- if (!name) continue;
136
+ if (!name) {
137
+ // Nothing to query by. Not an absence — chant never issued a read.
138
+ unobserved[entityName] = {
139
+ type: entityType,
140
+ reason: "read-failed",
141
+ detail: "declared entity has no metadata.name to query by",
142
+ };
143
+ continue;
144
+ }
104
145
 
105
146
  const nsArg = metadata.namespace ? ["-n", metadata.namespace] : [];
106
- const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, "-o", "json"].join(" ");
147
+ const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, ...ctxArg, "-o", "json"].join(" ");
107
148
 
108
149
  try {
109
150
  const { stdout } = await execAsync(cmd);
110
151
  const obj: KubectlResponse = JSON.parse(stdout);
111
- // owned filter: skip resources not carrying chant's marker label.
152
+ // owned filter: withhold resources not carrying chant's marker label.
153
+ // Withheld is not absent (#1089) — this object exists, it just isn't
154
+ // chant's, and dropping it silently is how `--owned` used to turn a
155
+ // declared-but-foreign resource into a proposed `create`.
112
156
  if (options.owned && !hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS)) {
157
+ unobserved[entityName] = {
158
+ type: entityType,
159
+ reason: "filtered",
160
+ detail: "live object carries no chant ownership marker and --owned was requested",
161
+ };
113
162
  continue;
114
163
  }
115
164
  result[entityName] = {
@@ -124,18 +173,23 @@ export async function describeResources(options: {
124
173
  resourceVersion: obj.metadata?.resourceVersion,
125
174
  }),
126
175
  };
127
- } catch {
128
- // Resource not found / kubectl error leave it out so state diff
129
- // can report it as missing. Don't fail the whole snapshot.
176
+ } catch (err) {
177
+ // Only a real NotFound leaves the entity out (an absence the diff may
178
+ // read as missing and the plan as create). Auth, connectivity, and every
179
+ // other failure prove nothing about existence and are reported as such.
180
+ const outcome = classifyKubectlFailure(err);
181
+ if (outcome.kind === "unobserved") {
182
+ unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
183
+ }
130
184
  }
131
185
  }
132
186
 
133
187
  if (skippedTypes.size > 0) {
134
188
  // eslint-disable-next-line no-console
135
189
  console.warn(
136
- `[k8s] skipped ${skippedTypes.size} entity type(s) without kubectl mapping: ${[...skippedTypes].join(", ")}`,
190
+ `[k8s] no kubectl mapping for ${skippedTypes.size} entity type(s): ${[...skippedTypes].join(", ")} — reported as unobserved (not absent), so no create is proposed for them`,
137
191
  );
138
192
  }
139
193
 
140
- return result;
194
+ return observation(result, unobserved);
141
195
  }
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";
@@ -31,6 +31,9 @@ vi.mock("node:child_process", async () => {
31
31
  const { k8sPlugin } = await import("./plugin");
32
32
  const { liveImportFromPlugins } = await import("@intentius/chant/cli/commands/import");
33
33
  const { buildChangeSet } = await import("@intentius/chant/lifecycle/change-set");
34
+ const { normalizeObservation } = await import("@intentius/chant/observation");
35
+ const { liveEvidenceFromChangeSet, reconcileStatus } = await import("@intentius/chant/lifecycle/status");
36
+ const { describeObservationConformance } = await import("@intentius/chant-test-utils");
34
37
 
35
38
  const liveDeployment = {
36
39
  apiVersion: "apps/v1",
@@ -82,14 +85,16 @@ describe("k8s lifecycle integration (#163)", () => {
82
85
  : new Error("not found"),
83
86
  );
84
87
 
85
- const observedNow = await k8sPlugin.describeResources!({
86
- environment: "prod",
87
- buildOutput: "",
88
- entityNames: ["web"],
89
- entities: new Map([
90
- ["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } }],
91
- ]),
92
- });
88
+ const { resources: observedNow } = normalizeObservation(
89
+ await k8sPlugin.describeResources!({
90
+ environment: "prod",
91
+ buildOutput: "",
92
+ entityNames: ["web"],
93
+ entities: new Map([
94
+ ["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } }],
95
+ ]),
96
+ }),
97
+ );
93
98
  expect(observedNow.web?.type).toBe("K8s::Apps::Deployment");
94
99
 
95
100
  const cs = buildChangeSet("prod", {
@@ -108,4 +113,130 @@ describe("k8s lifecycle integration (#163)", () => {
108
113
  });
109
114
  expect(cs2.entries.find((e) => e.name === "web")!.action).toBe("noop");
110
115
  });
116
+
117
+ /**
118
+ * The #1089 chain, end to end on the real plugin: a declared CRD the lexicon
119
+ * has no reader for goes describe → plan → component status without ever
120
+ * turning into a create or a "stale" component.
121
+ */
122
+ test("tri-state chain: an unreadable CRD stays unobserved through describe → plan → status (#1089)", async () => {
123
+ execMock.mockImplementation((cmd?: string) =>
124
+ cmd?.includes("deployment.apps web")
125
+ ? {
126
+ stdout: JSON.stringify({
127
+ metadata: { name: "web", namespace: "prod", uid: "uid-1" },
128
+ status: { readyReplicas: 3, replicas: 3 },
129
+ }),
130
+ stderr: "",
131
+ }
132
+ : new Error('Error from server (NotFound): deployments.apps "gone" not found'),
133
+ );
134
+
135
+ const entities = new Map([
136
+ ["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } }],
137
+ ["gone", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "gone", namespace: "prod" } } }],
138
+ ["widget", { entityType: "K8s::Example::Widget", props: { metadata: { name: "widget", namespace: "prod" } } }],
139
+ ]);
140
+
141
+ // 1. describe — three declared entities, three different verdicts.
142
+ const observed = normalizeObservation(
143
+ await k8sPlugin.describeResources!({
144
+ environment: "prod",
145
+ buildOutput: "",
146
+ entityNames: [...entities.keys()],
147
+ entities,
148
+ }),
149
+ );
150
+ expect(Object.keys(observed.resources)).toEqual(["web"]);
151
+ expect(observed.unobserved.widget.reason).toBe("unsupported-kind");
152
+ expect(observed.unobserved.gone).toBeUndefined(); // NotFound is an absence
153
+
154
+ // 2. plan — the CRD is `unobserved`; only the confirmed-absent one is a create.
155
+ const cs = buildChangeSet("prod", {
156
+ declared: new Set(entities.keys()),
157
+ observedNow: observed.resources,
158
+ observedThen: undefined,
159
+ unobserved: observed.unobserved,
160
+ });
161
+ const byName = Object.fromEntries(cs.entries.map((e) => [e.name, e.action]));
162
+ expect(byName).toEqual({ web: "noop", gone: "create", widget: "unobserved" });
163
+
164
+ // 3. status — a recorded component whose entity was never read reports
165
+ // `unknown`, not `stale`, and carries no `live` boolean at all.
166
+ const rows = reconcileStatus("prod", [
167
+ { component: "widget", env: "prod", digest: "sha256:abc", gitSha: "g", runId: "r", timestamp: "2026-01-01T00:00:00Z", actor: "ci" },
168
+ ], { liveEvidence: liveEvidenceFromChangeSet(cs) });
169
+ const widgetRow = rows.find((r) => r.component === "widget")!;
170
+ expect(widgetRow.reconciliation).toBe("unknown");
171
+ expect(widgetRow.live).toBeUndefined();
172
+ expect(widgetRow.unobserved?.reason).toBe("unsupported-kind");
173
+ expect(widgetRow.detail).toContain("could not be observed");
174
+ });
175
+ });
176
+
177
+ // The shared conformance suite (#1089) — every observing lexicon runs it.
178
+ describeObservationConformance({
179
+ lexicon: "k8s",
180
+ scenarios: [
181
+ {
182
+ name: "a CRD kind with no reader",
183
+ declared: ["widget", "gone"],
184
+ expectUnobserved: ["widget"],
185
+ expectAbsent: ["gone"],
186
+ run: () => {
187
+ execMock.mockImplementation(() => new Error('Error from server (NotFound): deployments.apps "gone" not found'));
188
+ return k8sPlugin.describeResources!({
189
+ environment: "prod",
190
+ buildOutput: "",
191
+ entityNames: ["widget", "gone"],
192
+ entities: new Map([
193
+ ["widget", { entityType: "K8s::Example::Widget", props: { metadata: { name: "widget" } } }],
194
+ ["gone", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "gone" } } }],
195
+ ]),
196
+ });
197
+ },
198
+ },
199
+ {
200
+ name: "an unreachable API server",
201
+ declared: ["web"],
202
+ expectUnobserved: ["web"],
203
+ run: () => {
204
+ execMock.mockImplementation(() =>
205
+ Object.assign(new Error("kubectl failed"), {
206
+ stderr: "The connection to the server 127.0.0.1:6443 was refused - did you specify the right host or port?",
207
+ }),
208
+ );
209
+ return k8sPlugin.describeResources!({
210
+ environment: "prod",
211
+ buildOutput: "",
212
+ entityNames: ["web"],
213
+ entities: new Map([
214
+ ["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } }],
215
+ ]),
216
+ });
217
+ },
218
+ },
219
+ {
220
+ name: "a healthy read",
221
+ declared: ["web"],
222
+ expectPresent: ["web"],
223
+ run: () => {
224
+ execMock.mockImplementation(() => ({
225
+ stdout: JSON.stringify({
226
+ metadata: { name: "web", namespace: "prod", uid: "uid-1", labels: { "app.kubernetes.io/managed-by": "chant" } },
227
+ status: { readyReplicas: 1, replicas: 1 },
228
+ }),
229
+ stderr: "",
230
+ }));
231
+ return k8sPlugin.describeResources!({
232
+ environment: "prod",
233
+ buildOutput: "",
234
+ entityNames: ["web"],
235
+ entities: new Map([
236
+ ["web", { entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } }],
237
+ ]),
238
+ });
239
+ },
240
+ },
241
+ ],
111
242
  });
@@ -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: