@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.
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +18 -5
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +6 -1
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +6 -1
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/skills/chant-k8s-eks.md +18 -3
- package/package.json +2 -2
- package/src/config.ts +63 -0
- package/src/describe-resources.test.ts +148 -11
- package/src/describe-resources.ts +67 -13
- package/src/index.ts +3 -0
- package/src/lifecycle-integration.test.ts +139 -8
- package/src/op/activities/index.ts +12 -0
- package/src/op/activities/kubectl.ts +6 -1
- package/src/op/activities/wait-for-ready.ts +6 -1
- package/src/skills/chant-k8s-eks.md +18 -3
package/dist/config.d.ts
ADDED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
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<
|
|
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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
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": "
|
|
53
|
+
"composite": "52fb1f56f334cf400a0544278a42f1d85d52aa9f49f55b4d845b9844e2e42aa4"
|
|
54
54
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
89
|
-
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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<
|
|
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)
|
|
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:
|
|
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
|
-
//
|
|
129
|
-
//
|
|
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]
|
|
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 =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 —
|
|
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
|
-
|
|
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:
|