@intentius/chant-lexicon-k8s 0.41.18 → 0.41.20
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/components/builders.d.ts +4 -0
- package/dist/components/builders.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/kustomize-apply.d.ts +48 -0
- package/dist/components/kustomize-apply.d.ts.map +1 -0
- 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 +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/kubectl.d.ts +15 -1
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +16 -6
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/builders.ts +5 -0
- package/src/components/capability-plugin.ts +3 -2
- package/src/components/index.ts +7 -0
- package/src/components/kustomize-apply.test.ts +98 -0
- package/src/components/kustomize-apply.ts +133 -0
- package/src/describe-resources.test.ts +159 -0
- package/src/describe-resources.ts +142 -2
- package/src/index.ts +1 -0
- package/src/op/activities/kubectl.test.ts +22 -0
- package/src/op/activities/kubectl.ts +15 -2
- package/src/op/activities/wait-for-ready.test.ts +27 -0
- package/src/op/activities/wait-for-ready.ts +84 -6
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* instead of a raw object literal.
|
|
12
12
|
*/
|
|
13
13
|
import type { KubectlApplyInput } from "./kubectl-apply.js";
|
|
14
|
+
import type { KustomizeApplyInput } from "./kustomize-apply.js";
|
|
14
15
|
export declare const kubectlApply: (input: KubectlApplyInput & {
|
|
15
16
|
noRollback?: string;
|
|
16
17
|
}) => import("@intentius/chant/components").Step;
|
|
18
|
+
export declare const kustomizeApply: (input: KustomizeApplyInput & {
|
|
19
|
+
noRollback?: string;
|
|
20
|
+
}) => import("@intentius/chant/components").Step;
|
|
17
21
|
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/components/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/components/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,eAAO,MAAM,YAAY;iBAA2C,MAAM;gDAAoB,CAAC;AAI/F,eAAO,MAAM,cAAc;iBAA6C,MAAM;gDAAsB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
2
2
|
export declare const K8S_VERB_FAMILIES: {
|
|
3
|
-
readonly apply: readonly ["kubectl-apply"];
|
|
3
|
+
readonly apply: readonly ["kubectl-apply", "kustomize-apply"];
|
|
4
4
|
};
|
|
5
5
|
export declare const k8sCapabilityPlugin: CapabilityPlugin;
|
|
6
6
|
//# sourceMappingURL=capability-plugin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAWA,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;
|
|
1
|
+
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAWA,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AAIzG,eAAO,MAAM,iBAAiB;;CAEpB,CAAC;AAEX,eAAO,MAAM,mBAAmB,EAAE,gBAWjC,CAAC"}
|
|
@@ -13,4 +13,5 @@
|
|
|
13
13
|
export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin.js";
|
|
14
14
|
export * from "./builders.js";
|
|
15
15
|
export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput, type KubectlApplyOutcome, } from "./kubectl-apply.js";
|
|
16
|
+
export { kustomizeApplyCapability, createKustomizeApplyCapability, renderCommand, type KustomizeApplyInput, type KustomizeApplyOutcome, } from "./kustomize-apply.js";
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,8BAA8B,EAC9B,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Capability } from "@intentius/chant/components/capability";
|
|
2
|
+
/** Injectable shell runner — the helm-upgrade seam, same buffer bound. */
|
|
3
|
+
export type KustomizeRunner = (command: string) => Promise<{
|
|
4
|
+
stdout: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** Structural mirrors of the activity module's shapes (see the module doc). */
|
|
7
|
+
interface AppliedRef {
|
|
8
|
+
apiVersion: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
name: string;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface KustomizeApplyOutcome {
|
|
14
|
+
fieldManager: string;
|
|
15
|
+
applied: AppliedRef[];
|
|
16
|
+
pruned: AppliedRef[];
|
|
17
|
+
}
|
|
18
|
+
interface ApplierArgs {
|
|
19
|
+
manifest: string;
|
|
20
|
+
documents?: Array<Record<string, unknown>>;
|
|
21
|
+
environment?: string;
|
|
22
|
+
stack?: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
deleteMode?: "never" | "owned-only" | "gated";
|
|
25
|
+
}
|
|
26
|
+
type Applier = (args: ApplierArgs) => Promise<KustomizeApplyOutcome>;
|
|
27
|
+
export interface KustomizeApplyInput {
|
|
28
|
+
/** The kustomization directory (holds `kustomization.yaml`) — an overlay or a base. */
|
|
29
|
+
dir: string;
|
|
30
|
+
/**
|
|
31
|
+
* The deploy unit / ownership stack — same double duty as kubectl-apply's.
|
|
32
|
+
* Omitted derives from the project's `ownership.stack`, but then the unit is
|
|
33
|
+
* invisible to `components status --live`. Name it.
|
|
34
|
+
*/
|
|
35
|
+
stack?: string;
|
|
36
|
+
/** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
|
|
37
|
+
context?: string;
|
|
38
|
+
/** Same delete vocabulary as kubectl-apply; default `never`. */
|
|
39
|
+
delete?: "never" | "owned-only";
|
|
40
|
+
}
|
|
41
|
+
/** The exact render command, standalone binary first. Pure; exported for tests. */
|
|
42
|
+
export declare function renderCommand(dir: string, tool?: "kustomize" | "kubectl"): string;
|
|
43
|
+
/** Factory with injectable renderer + applier, so tests assert the render
|
|
44
|
+
* command and the exact applier delegation without kustomize or a cluster. */
|
|
45
|
+
export declare function createKustomizeApplyCapability(run?: KustomizeRunner, apply?: Applier): Capability<KustomizeApplyInput, KustomizeApplyOutcome>;
|
|
46
|
+
export declare const kustomizeApplyCapability: Capability<KustomizeApplyInput, KustomizeApplyOutcome>;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=kustomize-apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kustomize-apply.d.ts","sourceRoot":"","sources":["../../src/components/kustomize-apply.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,wCAAwC,CAAC;AAIxF,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAQ/E,+EAA+E;AAC/E,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;CAC/C;AAED,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;CACjC;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,WAAW,GAAG,SAAuB,GAAG,MAAM,CAE9F;AAoBD;8EAC8E;AAC9E,wBAAgB,8BAA8B,CAC5C,GAAG,GAAE,eAA+B,EACpC,KAAK,CAAC,EAAE,OAAO,GACd,UAAU,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAmBxD;AAED,eAAO,MAAM,wBAAwB,wDAAmC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAGtG,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAGtG,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CA2CvD;AA4FD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAetE;AAQD,MAAM,WAAW,wBAAwB;IACvC,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;IAChB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAmI5B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { k8sSerializer } from "./serializer.js";
|
|
|
2
2
|
export { k8sPlugin } from "./plugin.js";
|
|
3
3
|
export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./components/capability-plugin.js";
|
|
4
4
|
export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput } from "./components/kubectl-apply.js";
|
|
5
|
+
export { kustomizeApplyCapability, createKustomizeApplyCapability, type KustomizeApplyInput } from "./components/kustomize-apply.js";
|
|
5
6
|
export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
|
|
6
7
|
export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels.js";
|
|
7
8
|
export { K8sLabels, K8sAnnotations } from "./variables.js";
|
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;AAKrC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,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": "5dc7c047316cefafc266f579d7a266cfed2d3682a197bef9dbcb0f27c70af2df",
|
|
5
5
|
"meta.json": "c0b141b882c51483c1d1aead748a0eb171aceca7432edd2a0404b062bf6d7831",
|
|
6
6
|
"types/index.d.ts": "3be2af7494aca94adfa16269cb9c4e7a155529872207a3d70b0c4d39914f9eec",
|
|
7
7
|
"rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"skills/chant-k8s-aks.md": "e18f0e2b055f72cd7a37deaf258d7027c2d4d3e286e8fd4975b27a1f981a3ad9",
|
|
51
51
|
"skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5"
|
|
52
52
|
},
|
|
53
|
-
"composite": "
|
|
53
|
+
"composite": "624054fff650a57fd0f5c234a26aa362e45fed534780e98405f4ebae4c2470d0"
|
|
54
54
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* - **Documents apply in file order**, as `kubectl apply -f` does, and a
|
|
31
31
|
* directory's files are read in sorted order.
|
|
32
32
|
*/
|
|
33
|
+
import type { K8sObject } from "@intentius/chant-k8s-client";
|
|
33
34
|
import { type K8sConnector } from "../../api/connect.js";
|
|
34
35
|
/**
|
|
35
36
|
* How the apply treats chant-owned objects that are no longer declared. The
|
|
@@ -39,8 +40,21 @@ import { type K8sConnector } from "../../api/connect.js";
|
|
|
39
40
|
*/
|
|
40
41
|
export type ApplyDeleteMode = "never" | "owned-only" | "gated";
|
|
41
42
|
export interface KubectlApplyArgs {
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Path to a manifest file, or a directory of them. With `documents` given,
|
|
45
|
+
* this becomes only the human-facing label the heartbeats and logs carry
|
|
46
|
+
* (e.g. `kustomize:<dir>`), and nothing is read from disk.
|
|
47
|
+
*/
|
|
43
48
|
manifest: string;
|
|
49
|
+
/**
|
|
50
|
+
* Already-parsed documents to apply INSTEAD of reading `manifest` (#1548):
|
|
51
|
+
* a renderer that produced the objects in memory — kustomize-apply's
|
|
52
|
+
* `kustomize build` — hands them straight in rather than round-tripping
|
|
53
|
+
* through a temp file. Everything downstream (ownership stamping, the
|
|
54
|
+
* marker-scoped prune, the self-conflict retake) is document-driven and
|
|
55
|
+
* behaves identically.
|
|
56
|
+
*/
|
|
57
|
+
documents?: K8sObject[];
|
|
44
58
|
/**
|
|
45
59
|
* kubectl context name. Uses the ambient context if omitted. To target the
|
|
46
60
|
* same cluster the read path (`describeResources`) resolved for an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"kubectl.d.ts","sourceRoot":"","sources":["../../../src/op/activities/kubectl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAYH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC;IACxB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sFAAsF;IACtF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAiB7E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF;AAqHD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAkE9B;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
|
@@ -32,7 +32,22 @@ export interface PathMatch {
|
|
|
32
32
|
/** Holds when the value is one of these. */
|
|
33
33
|
oneOf?: Array<string | number>;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Match a `status.conditions[]` entry by `type` AND its `reason` (#1549).
|
|
37
|
+
* The dot-path walk can't index into a conditions array, and the Flux
|
|
38
|
+
* toolkit's failure signal is exactly `conditions[type=Ready].reason` — a
|
|
39
|
+
* wedge like `BuildFailed` keeps `Ready=False` forever while the generic
|
|
40
|
+
* poll waits out its whole timeout. `status` defaults to "False": a reason
|
|
41
|
+
* on a True condition is progress vocabulary, not a wedge.
|
|
42
|
+
*/
|
|
43
|
+
export interface ConditionReasonMatch {
|
|
44
|
+
conditionType: string;
|
|
45
|
+
/** Required `status` of the condition (default "False"). */
|
|
46
|
+
status?: string;
|
|
47
|
+
/** Holds when the condition's `reason` is one of these. */
|
|
48
|
+
reasonOneOf: string[];
|
|
49
|
+
}
|
|
50
|
+
export type ReadinessMatch = ConditionMatch | PathMatch | ConditionReasonMatch;
|
|
36
51
|
export interface ReadinessSpec {
|
|
37
52
|
/** All must hold for the resource to be ready. */
|
|
38
53
|
ready: ReadinessMatch[];
|
|
@@ -50,11 +65,6 @@ export interface ReadinessSpec {
|
|
|
50
65
|
* and the CockroachDB operator.
|
|
51
66
|
*/
|
|
52
67
|
export declare const DEFAULT_READINESS: ReadinessSpec;
|
|
53
|
-
/**
|
|
54
|
-
* Per-resource overrides keyed by `"<group>/<kind>"`. Argo's `Application`
|
|
55
|
-
* reports `health`/`sync`, not a `Ready` condition — the case that proves the
|
|
56
|
-
* override is necessary (#365).
|
|
57
|
-
*/
|
|
58
68
|
export declare const READINESS_OVERRIDES: Record<string, ReadinessSpec>;
|
|
59
69
|
/** Resolve the readiness spec for a resource: registry override, else default. */
|
|
60
70
|
export declare function readinessFor(group: string | undefined, kind: string): ReadinessSpec;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wait-for-ready.d.ts","sourceRoot":"","sources":["../../../src/op/activities/wait-for-ready.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;;;;;;;;;;;GAiBG;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;
|
|
1
|
+
{"version":3,"file":"wait-for-ready.d.ts","sourceRoot":"","sources":["../../../src/op/activities/wait-for-ready.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-k8s",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.20",
|
|
4
4
|
"description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"zod": "^4.3.6",
|
|
87
|
-
"@intentius/chant": "^0.41.
|
|
87
|
+
"@intentius/chant": "^0.41.20",
|
|
88
88
|
"typescript": "^5.9.3"
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -13,5 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { step } from "@intentius/chant/components";
|
|
15
15
|
import type { KubectlApplyInput } from "./kubectl-apply";
|
|
16
|
+
import type { KustomizeApplyInput } from "./kustomize-apply";
|
|
16
17
|
|
|
17
18
|
export const kubectlApply = step<KubectlApplyInput & { noRollback?: string }>("kubectl-apply");
|
|
19
|
+
|
|
20
|
+
// Same needs-opt-out posture, same sugar (#1548): a kustomize render in front
|
|
21
|
+
// of the identical server-side apply.
|
|
22
|
+
export const kustomizeApply = step<KustomizeApplyInput & { noRollback?: string }>("kustomize-apply");
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import type { Capability } from "@intentius/chant/components/capability";
|
|
12
12
|
import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
13
13
|
import { kubectlApplyCapability } from "./kubectl-apply";
|
|
14
|
+
import { kustomizeApplyCapability } from "./kustomize-apply";
|
|
14
15
|
|
|
15
16
|
export const K8S_VERB_FAMILIES = {
|
|
16
|
-
apply: ["kubectl-apply"],
|
|
17
|
+
apply: ["kubectl-apply", "kustomize-apply"],
|
|
17
18
|
} as const;
|
|
18
19
|
|
|
19
20
|
export const k8sCapabilityPlugin: CapabilityPlugin = {
|
|
@@ -22,7 +23,7 @@ export const k8sCapabilityPlugin: CapabilityPlugin = {
|
|
|
22
23
|
// a literal here was stale one release after it was written.
|
|
23
24
|
version: ownPackageVersion(import.meta.url),
|
|
24
25
|
capabilities(): Array<Capability<never, unknown>> {
|
|
25
|
-
return [kubectlApplyCapability as Capability<never, unknown>];
|
|
26
|
+
return [kubectlApplyCapability as Capability<never, unknown>, kustomizeApplyCapability as Capability<never, unknown>];
|
|
26
27
|
},
|
|
27
28
|
families(): Record<string, readonly string[]> {
|
|
28
29
|
return K8S_VERB_FAMILIES;
|
package/src/components/index.ts
CHANGED
|
@@ -19,3 +19,10 @@ export {
|
|
|
19
19
|
type KubectlApplyInput,
|
|
20
20
|
type KubectlApplyOutcome,
|
|
21
21
|
} from "./kubectl-apply";
|
|
22
|
+
export {
|
|
23
|
+
kustomizeApplyCapability,
|
|
24
|
+
createKustomizeApplyCapability,
|
|
25
|
+
renderCommand,
|
|
26
|
+
type KustomizeApplyInput,
|
|
27
|
+
type KustomizeApplyOutcome,
|
|
28
|
+
} from "./kustomize-apply";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kustomize-apply` (#1548) — the render-then-apply leaf. What must hold:
|
|
3
|
+
* the capability registers under its kind with the lexicon's own version
|
|
4
|
+
* (#1505), the render command is exact and falls back to kubectl's vendored
|
|
5
|
+
* kustomize, the rendered documents reach the applier INLINE (no temp file)
|
|
6
|
+
* with the deploy-unit stack, and the rollback posture matches kubectl-apply
|
|
7
|
+
* (server-side apply has no native undo).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { createKustomizeApplyCapability, kustomizeApplyCapability, renderCommand, type KustomizeApplyInput } from "./kustomize-apply";
|
|
11
|
+
import { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin";
|
|
12
|
+
import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
13
|
+
import type { DeployContext } from "@intentius/chant/components/capability";
|
|
14
|
+
|
|
15
|
+
const ctx = { env: "dev" } as DeployContext;
|
|
16
|
+
|
|
17
|
+
const RENDERED = `apiVersion: v1
|
|
18
|
+
kind: Namespace
|
|
19
|
+
metadata:
|
|
20
|
+
name: web
|
|
21
|
+
---
|
|
22
|
+
apiVersion: apps/v1
|
|
23
|
+
kind: Deployment
|
|
24
|
+
metadata:
|
|
25
|
+
name: app
|
|
26
|
+
namespace: web
|
|
27
|
+
spec:
|
|
28
|
+
replicas: 1
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
describe("kustomize-apply capability (#1548)", () => {
|
|
32
|
+
test("registers on the k8s plugin, in the apply family, at the package's own version", () => {
|
|
33
|
+
expect(isCapabilityPlugin(k8sCapabilityPlugin)).toBe(true);
|
|
34
|
+
expect(k8sCapabilityPlugin.capabilities().map((c) => c.kind)).toContain("kustomize-apply");
|
|
35
|
+
expect(K8S_VERB_FAMILIES.apply).toContain("kustomize-apply");
|
|
36
|
+
// #1505 — never a stale literal.
|
|
37
|
+
const { version } = JSON.parse(
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
|
+
require("node:fs").readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
|
|
40
|
+
) as { version: string };
|
|
41
|
+
expect(k8sCapabilityPlugin.version).toBe(version);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rollback is needs-opt-out — a server-side apply keeps no previous state", () => {
|
|
45
|
+
expect(kustomizeApplyCapability.rollbackPolicy).toBe("needs-opt-out");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("renders with kustomize build and hands the parsed documents to the applier inline", async () => {
|
|
49
|
+
const commands: string[] = [];
|
|
50
|
+
const applierArgs: unknown[] = [];
|
|
51
|
+
const cap = createKustomizeApplyCapability(
|
|
52
|
+
async (command) => {
|
|
53
|
+
commands.push(command);
|
|
54
|
+
return { stdout: RENDERED };
|
|
55
|
+
},
|
|
56
|
+
async (args) => {
|
|
57
|
+
applierArgs.push(args);
|
|
58
|
+
return { fieldManager: "chant:web", applied: [], pruned: [] };
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await cap.run(ctx, { dir: "overlays/dev", stack: "web", delete: "owned-only" } as KustomizeApplyInput as never);
|
|
63
|
+
|
|
64
|
+
expect(commands).toEqual(["kustomize build 'overlays/dev'"]);
|
|
65
|
+
expect(applierArgs).toHaveLength(1);
|
|
66
|
+
const args = applierArgs[0] as { manifest: string; documents: unknown[]; environment: string; stack: string; deleteMode: string };
|
|
67
|
+
expect(args.manifest).toBe("kustomize:overlays/dev"); // a label, not a path
|
|
68
|
+
expect(args.documents).toHaveLength(2);
|
|
69
|
+
expect((args.documents[1] as { kind: string }).kind).toBe("Deployment");
|
|
70
|
+
expect(args.environment).toBe("dev");
|
|
71
|
+
expect(args.stack).toBe("web");
|
|
72
|
+
expect(args.deleteMode).toBe("owned-only");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("falls back to kubectl's vendored kustomize when the binary is missing", async () => {
|
|
76
|
+
const commands: string[] = [];
|
|
77
|
+
const cap = createKustomizeApplyCapability(
|
|
78
|
+
async (command) => {
|
|
79
|
+
commands.push(command);
|
|
80
|
+
if (command.startsWith("kustomize ")) throw new Error("spawn kustomize ENOENT");
|
|
81
|
+
return { stdout: RENDERED };
|
|
82
|
+
},
|
|
83
|
+
async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
|
|
84
|
+
);
|
|
85
|
+
await cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never);
|
|
86
|
+
expect(commands).toEqual([renderCommand("overlays/dev", "kustomize"), renderCommand("overlays/dev", "kubectl")]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a render failure that is NOT a missing binary propagates — a broken overlay is not silently retried", async () => {
|
|
90
|
+
const cap = createKustomizeApplyCapability(
|
|
91
|
+
async () => {
|
|
92
|
+
throw new Error("accumulating resources: missing base ../common");
|
|
93
|
+
},
|
|
94
|
+
async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
|
|
95
|
+
);
|
|
96
|
+
await expect(cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never)).rejects.toThrow("missing base");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kustomize-apply` — the kustomize leaf for the component model (#1548).
|
|
3
|
+
*
|
|
4
|
+
* Kustomize is a RENDERER, not a substrate: `kustomize build <dir>` emits
|
|
5
|
+
* exactly the Kubernetes manifests the existing apply pipeline was built for.
|
|
6
|
+
* So this capability is a render step in front of `kubectl-apply`'s machinery
|
|
7
|
+
* — the rendered documents go straight to the same server-side applier
|
|
8
|
+
* (inline, no temp file), which stamps the deploy-unit stack (#1539), prunes
|
|
9
|
+
* marker-scoped, retakes chant's own field conflicts (#1541/#1542), and is
|
|
10
|
+
* observed by the same `describeStackStatus` label sweep. An estate that
|
|
11
|
+
* keeps its overlay tree gets components-status honesty without adopting a
|
|
12
|
+
* single typed manifest.
|
|
13
|
+
*
|
|
14
|
+
* `stack` doubles as the ownership identity and the deploy unit, exactly as
|
|
15
|
+
* `kubectl-apply` — one name, both jobs.
|
|
16
|
+
*
|
|
17
|
+
* Renderer resolution: `kustomize build`, falling back to
|
|
18
|
+
* `kubectl kustomize` (the same renderer vendored into kubectl) when the
|
|
19
|
+
* standalone binary is absent. Both run through an injectable runner so tests
|
|
20
|
+
* assert the exact command without either binary installed. The 64MiB
|
|
21
|
+
* maxBuffer mirrors helm-upgrade's — a big overlay renders megabytes.
|
|
22
|
+
*
|
|
23
|
+
* The #1074 boundary applies as it does to kubectl-apply: nothing exported
|
|
24
|
+
* from the lexicon entry point may statically import the API-client chain, so
|
|
25
|
+
* the applier is reached by dynamic import inside `run()` and its argument
|
|
26
|
+
* shape is a structural mirror.
|
|
27
|
+
*/
|
|
28
|
+
import { exec } from "node:child_process";
|
|
29
|
+
import { promisify } from "node:util";
|
|
30
|
+
import { loadAll } from "js-yaml";
|
|
31
|
+
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
32
|
+
|
|
33
|
+
const execAsync = promisify(exec);
|
|
34
|
+
|
|
35
|
+
/** Injectable shell runner — the helm-upgrade seam, same buffer bound. */
|
|
36
|
+
export type KustomizeRunner = (command: string) => Promise<{ stdout: string }>;
|
|
37
|
+
const defaultRunner: KustomizeRunner = (command) => execAsync(command, { maxBuffer: 64 * 1024 * 1024 });
|
|
38
|
+
|
|
39
|
+
/** Single-quote shell escaping, as helm-upgrade quotes its argv. */
|
|
40
|
+
function q(v: string): string {
|
|
41
|
+
return `'${v.replace(/'/g, "'\\''")}'`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Structural mirrors of the activity module's shapes (see the module doc). */
|
|
45
|
+
interface AppliedRef {
|
|
46
|
+
apiVersion: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
name: string;
|
|
49
|
+
namespace?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface KustomizeApplyOutcome {
|
|
53
|
+
fieldManager: string;
|
|
54
|
+
applied: AppliedRef[];
|
|
55
|
+
pruned: AppliedRef[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ApplierArgs {
|
|
59
|
+
manifest: string;
|
|
60
|
+
documents?: Array<Record<string, unknown>>;
|
|
61
|
+
environment?: string;
|
|
62
|
+
stack?: string;
|
|
63
|
+
context?: string;
|
|
64
|
+
deleteMode?: "never" | "owned-only" | "gated";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Applier = (args: ApplierArgs) => Promise<KustomizeApplyOutcome>;
|
|
68
|
+
|
|
69
|
+
export interface KustomizeApplyInput {
|
|
70
|
+
/** The kustomization directory (holds `kustomization.yaml`) — an overlay or a base. */
|
|
71
|
+
dir: string;
|
|
72
|
+
/**
|
|
73
|
+
* The deploy unit / ownership stack — same double duty as kubectl-apply's.
|
|
74
|
+
* Omitted derives from the project's `ownership.stack`, but then the unit is
|
|
75
|
+
* invisible to `components status --live`. Name it.
|
|
76
|
+
*/
|
|
77
|
+
stack?: string;
|
|
78
|
+
/** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
|
|
79
|
+
context?: string;
|
|
80
|
+
/** Same delete vocabulary as kubectl-apply; default `never`. */
|
|
81
|
+
delete?: "never" | "owned-only";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The exact render command, standalone binary first. Pure; exported for tests. */
|
|
85
|
+
export function renderCommand(dir: string, tool: "kustomize" | "kubectl" = "kustomize"): string {
|
|
86
|
+
return tool === "kustomize" ? `kustomize build ${q(dir)}` : `kubectl kustomize ${q(dir)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Render the overlay, falling back to kubectl's vendored kustomize when the
|
|
90
|
+
* standalone binary is missing (ENOENT/127), and parse the emitted YAML. */
|
|
91
|
+
async function renderDocuments(dir: string, run: KustomizeRunner): Promise<Array<Record<string, unknown>>> {
|
|
92
|
+
let stdout: string;
|
|
93
|
+
try {
|
|
94
|
+
({ stdout } = await run(renderCommand(dir, "kustomize")));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
if (!/ENOENT|not found|command not found|127/.test(message)) throw err;
|
|
98
|
+
({ stdout } = await run(renderCommand(dir, "kubectl")));
|
|
99
|
+
}
|
|
100
|
+
const documents: Array<Record<string, unknown>> = [];
|
|
101
|
+
for (const doc of loadAll(stdout)) {
|
|
102
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc)) documents.push(doc as Record<string, unknown>);
|
|
103
|
+
}
|
|
104
|
+
return documents;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Factory with injectable renderer + applier, so tests assert the render
|
|
108
|
+
* command and the exact applier delegation without kustomize or a cluster. */
|
|
109
|
+
export function createKustomizeApplyCapability(
|
|
110
|
+
run: KustomizeRunner = defaultRunner,
|
|
111
|
+
apply?: Applier,
|
|
112
|
+
): Capability<KustomizeApplyInput, KustomizeApplyOutcome> {
|
|
113
|
+
return {
|
|
114
|
+
kind: "kustomize-apply",
|
|
115
|
+
// Server-side apply keeps no previous object state, and kustomize adds no
|
|
116
|
+
// undo of its own — same posture as kubectl-apply.
|
|
117
|
+
rollbackPolicy: "needs-opt-out",
|
|
118
|
+
async run(ctx: DeployContext, input: KustomizeApplyInput): Promise<KustomizeApplyOutcome> {
|
|
119
|
+
const documents = await renderDocuments(input.dir, run);
|
|
120
|
+
const applier: Applier = apply ?? ((await import("../op/activities/kubectl")).applyManifest as unknown as Applier);
|
|
121
|
+
return applier({
|
|
122
|
+
manifest: `kustomize:${input.dir}`,
|
|
123
|
+
documents,
|
|
124
|
+
environment: ctx.env,
|
|
125
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
126
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
127
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const kustomizeApplyCapability = createKustomizeApplyCapability();
|
|
@@ -573,6 +573,135 @@ describe("k8s describeResources", () => {
|
|
|
573
573
|
expect(result.resources["prod/hand-made"]).toBeUndefined();
|
|
574
574
|
});
|
|
575
575
|
|
|
576
|
+
test("a Flux-managed Deployment is attributed to its declared Kustomization by the controller's labels (#1549)", async () => {
|
|
577
|
+
// Flux stamps managed objects with name+namespace labels instead of
|
|
578
|
+
// ownerReferences, so the chain walk reaches nothing — the label
|
|
579
|
+
// channel resolves against the DECLARED CR, exactly. The workload also
|
|
580
|
+
// lives in a namespace the estate never declares an entity in: the
|
|
581
|
+
// Kustomization's own spec.targetNamespace is what puts it in scope.
|
|
582
|
+
const kustomization = {
|
|
583
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
584
|
+
kind: "Kustomization",
|
|
585
|
+
metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
|
|
586
|
+
status: { conditions: [{ type: "Ready", status: "True" }] },
|
|
587
|
+
};
|
|
588
|
+
const managed = {
|
|
589
|
+
apiVersion: "apps/v1",
|
|
590
|
+
kind: "Deployment",
|
|
591
|
+
metadata: {
|
|
592
|
+
name: "web",
|
|
593
|
+
namespace: "prod",
|
|
594
|
+
uid: "dep-uid",
|
|
595
|
+
labels: { "kustomize.toolkit.fluxcd.io/name": "apps", "kustomize.toolkit.fluxcd.io/namespace": "flux-system" },
|
|
596
|
+
},
|
|
597
|
+
status: { readyReplicas: 1, replicas: 1 },
|
|
598
|
+
};
|
|
599
|
+
const cluster = fakeCluster({
|
|
600
|
+
objects: {
|
|
601
|
+
[objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
|
|
602
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const result = await describeResources(
|
|
607
|
+
{
|
|
608
|
+
environment: "prod",
|
|
609
|
+
buildOutput: "",
|
|
610
|
+
entityNames: ["apps"],
|
|
611
|
+
entities: makeEntities([
|
|
612
|
+
{
|
|
613
|
+
name: "apps",
|
|
614
|
+
entityType: "K8s::Flux::Kustomization",
|
|
615
|
+
props: { metadata: { name: "apps", namespace: "flux-system" }, spec: { targetNamespace: "prod", sourceRef: { kind: "GitRepository", name: "infra" } } },
|
|
616
|
+
},
|
|
617
|
+
]),
|
|
618
|
+
},
|
|
619
|
+
cluster.connector,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
expect(result.resources["prod/web"]).toMatchObject({
|
|
623
|
+
type: "K8s::Apps::Deployment",
|
|
624
|
+
ownerChain: { root: "declared", entity: "apps" },
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("Argo's bare instance label claims only a UNIQUE declared Application (#1549)", async () => {
|
|
629
|
+
const app = (name: string) => ({
|
|
630
|
+
apiVersion: "argoproj.io/v1alpha1",
|
|
631
|
+
kind: "Application",
|
|
632
|
+
metadata: { name, namespace: "argocd", uid: `${name}-uid` },
|
|
633
|
+
status: { health: { status: "Healthy" }, sync: { status: "Synced" } },
|
|
634
|
+
});
|
|
635
|
+
const managed = {
|
|
636
|
+
apiVersion: "apps/v1",
|
|
637
|
+
kind: "Deployment",
|
|
638
|
+
metadata: { name: "web", namespace: "prod", uid: "dep-uid", labels: { "app.kubernetes.io/instance": "web" } },
|
|
639
|
+
};
|
|
640
|
+
const cluster = fakeCluster({
|
|
641
|
+
objects: {
|
|
642
|
+
[objectKey("argoproj.io/v1alpha1", "Application", "web", "argocd")]: app("web"),
|
|
643
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const result = await describeResources(
|
|
648
|
+
{
|
|
649
|
+
environment: "prod",
|
|
650
|
+
buildOutput: "",
|
|
651
|
+
entityNames: ["webApp"],
|
|
652
|
+
entities: makeEntities([
|
|
653
|
+
{
|
|
654
|
+
name: "webApp",
|
|
655
|
+
entityType: "K8s::Argo::Application",
|
|
656
|
+
props: { metadata: { name: "web", namespace: "argocd" }, spec: { destination: { namespace: "prod" } } },
|
|
657
|
+
},
|
|
658
|
+
]),
|
|
659
|
+
},
|
|
660
|
+
cluster.connector,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(result.resources["prod/web"]).toMatchObject({
|
|
664
|
+
type: "K8s::Apps::Deployment",
|
|
665
|
+
ownerChain: { root: "declared", entity: "webApp" },
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a generic instance label matching NO declared Application claims nothing — not an inventory (#1549)", async () => {
|
|
670
|
+
// `app.kubernetes.io/instance` is also what plain helm sets; on an
|
|
671
|
+
// estate declaring a Flux CR (so the widened sweep runs), a foreign
|
|
672
|
+
// helm-installed Deployment must not be attributed.
|
|
673
|
+
const kustomization = {
|
|
674
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
675
|
+
kind: "Kustomization",
|
|
676
|
+
metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
|
|
677
|
+
};
|
|
678
|
+
const foreign = {
|
|
679
|
+
apiVersion: "apps/v1",
|
|
680
|
+
kind: "Deployment",
|
|
681
|
+
metadata: { name: "cert-manager", namespace: "flux-system", uid: "cm-uid", labels: { "app.kubernetes.io/instance": "cert-manager" } },
|
|
682
|
+
};
|
|
683
|
+
const cluster = fakeCluster({
|
|
684
|
+
objects: {
|
|
685
|
+
[objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
|
|
686
|
+
[objectKey("apps/v1", "Deployment", "cert-manager", "flux-system")]: foreign,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const result = await describeResources(
|
|
691
|
+
{
|
|
692
|
+
environment: "prod",
|
|
693
|
+
buildOutput: "",
|
|
694
|
+
entityNames: ["apps"],
|
|
695
|
+
entities: makeEntities([
|
|
696
|
+
{ name: "apps", entityType: "K8s::Flux::Kustomization", props: { metadata: { name: "apps", namespace: "flux-system" } } },
|
|
697
|
+
]),
|
|
698
|
+
},
|
|
699
|
+
cluster.connector,
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
expect(result.resources["flux-system/cert-manager"]).toBeUndefined();
|
|
703
|
+
});
|
|
704
|
+
|
|
576
705
|
test("a core-only estate sweeps no extra groups — Pods stay the core representative", async () => {
|
|
577
706
|
const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod", uid: "svc-uid" } };
|
|
578
707
|
const cluster = fakeCluster({
|
|
@@ -880,6 +1009,36 @@ describe("statusFromObject — the most specific failing signal (#1397)", () =>
|
|
|
880
1009
|
expect(statusFromObject({ status: { readyReplicas: 1, replicas: 3 } } as never)).toBe("PROGRESSING(1/3)");
|
|
881
1010
|
expect(statusFromObject({} as never)).toBe("PRESENT");
|
|
882
1011
|
});
|
|
1012
|
+
|
|
1013
|
+
test("a conditions-first CR reads its Ready condition, not PRESENT (#1549)", () => {
|
|
1014
|
+
// A wedged Flux Kustomization/HelmRelease has no phase and no replica
|
|
1015
|
+
// counts, so it fell straight to PRESENT — a wedged reconciler read
|
|
1016
|
+
// exactly like a healthy one.
|
|
1017
|
+
expect(
|
|
1018
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "False", reason: "BuildFailed" }] } } as never),
|
|
1019
|
+
).toBe("BuildFailed");
|
|
1020
|
+
expect(
|
|
1021
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1022
|
+
).toBe("NOT-READY");
|
|
1023
|
+
expect(
|
|
1024
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "True", reason: "ReconciliationSucceeded" }] } } as never),
|
|
1025
|
+
).toBe("READY");
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("the Ready rung never changes a Pod or workload word — phase and replicas rank above it (#1549)", () => {
|
|
1029
|
+
expect(
|
|
1030
|
+
statusFromObject({ status: { phase: "Running", conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1031
|
+
).toBe("Running");
|
|
1032
|
+
expect(
|
|
1033
|
+
statusFromObject({ status: { readyReplicas: 1, replicas: 3, conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1034
|
+
).toBe("PROGRESSING(1/3)");
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test("an Argo Application reads health/sync (#1549)", () => {
|
|
1038
|
+
expect(statusFromObject({ status: { health: { status: "Degraded" }, sync: { status: "Synced" } } } as never)).toBe("Degraded");
|
|
1039
|
+
expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "OutOfSync" } } } as never)).toBe("OutOfSync");
|
|
1040
|
+
expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "Synced" } } } as never)).toBe("READY");
|
|
1041
|
+
});
|
|
883
1042
|
});
|
|
884
1043
|
|
|
885
1044
|
// #1401 — #1397 made the status WORD honest; this carries the part that says
|
|
@@ -104,9 +104,54 @@ export function statusFromObject(obj: K8sObject): string {
|
|
|
104
104
|
? "READY"
|
|
105
105
|
: `PROGRESSING(${status.readyReplicas}/${status.replicas})`;
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
// The kstatus rung (#1549): an object with NO phase and NO replica counts —
|
|
109
|
+
// a Flux Kustomization/HelmRelease, cert-manager, any conditions-first CRD —
|
|
110
|
+
// used to fall straight to PRESENT, so a wedged reconciler read exactly like
|
|
111
|
+
// a healthy one. `Ready` is the contract those objects speak: False → its
|
|
112
|
+
// reason (`BuildFailed`, `UpgradeFailed`, …), True → READY. Deliberately
|
|
113
|
+
// BELOW the phase/replica rungs so no Pod or workload word changes — for
|
|
114
|
+
// those objects a False Ready is already visible through the rungs above.
|
|
115
|
+
const ready = readyConditionWord(status);
|
|
116
|
+
if (ready) return ready;
|
|
117
|
+
|
|
118
|
+
// Argo's Application speaks health/sync, not a Ready condition (the same
|
|
119
|
+
// split the readiness registry encodes) — `Degraded` / `OutOfSync` beats
|
|
120
|
+
// PRESENT for the one object whose whole job is convergence.
|
|
121
|
+
const argo = argoStatusWord(status);
|
|
122
|
+
if (argo) return argo;
|
|
123
|
+
|
|
107
124
|
return "PRESENT";
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
/** The `Ready` condition rendered as a status word: False → its reason (or
|
|
128
|
+
* NOT-READY when the controller gave none), True → READY, absent → nothing. */
|
|
129
|
+
function readyConditionWord(status: K8sObject["status"]): string | undefined {
|
|
130
|
+
const conditions = (status as { conditions?: unknown } | undefined)?.conditions;
|
|
131
|
+
if (!Array.isArray(conditions)) return undefined;
|
|
132
|
+
for (const c of conditions) {
|
|
133
|
+
const cond = c as { type?: unknown; status?: unknown; reason?: unknown };
|
|
134
|
+
if (cond.type !== "Ready") continue;
|
|
135
|
+
if (cond.status === "True") return "READY";
|
|
136
|
+
if (cond.status === "False") {
|
|
137
|
+
return typeof cond.reason === "string" && cond.reason.length > 0 ? cond.reason : "NOT-READY";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Argo Application health/sync as a status word: anything but the happy pair
|
|
144
|
+
* surfaces the unhappy half; Healthy+Synced reads as READY. */
|
|
145
|
+
function argoStatusWord(status: K8sObject["status"]): string | undefined {
|
|
146
|
+
const s = status as { health?: { status?: unknown }; sync?: { status?: unknown } } | undefined;
|
|
147
|
+
const health = typeof s?.health?.status === "string" ? s.health.status : undefined;
|
|
148
|
+
const sync = typeof s?.sync?.status === "string" ? s.sync.status : undefined;
|
|
149
|
+
if (!health && !sync) return undefined;
|
|
150
|
+
if (health && health !== "Healthy") return health;
|
|
151
|
+
if (sync && sync !== "Synced") return sync;
|
|
152
|
+
return "READY";
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
/**
|
|
111
156
|
* The reason a container is stuck, if one is — `CrashLoopBackOff`,
|
|
112
157
|
* `ImagePullBackOff`, `CreateContainerConfigError`.
|
|
@@ -333,7 +378,7 @@ export async function describeResources(
|
|
|
333
378
|
if (op && op.apiVersion.includes("/")) runtimeGroupVersions.add(op.apiVersion);
|
|
334
379
|
}
|
|
335
380
|
|
|
336
|
-
await addRuntimeChildren(client, resources, options.owned, runtimeGroupVersions);
|
|
381
|
+
await addRuntimeChildren(client, resources, options.owned, runtimeGroupVersions, declared);
|
|
337
382
|
|
|
338
383
|
return observation(resources, unobserved);
|
|
339
384
|
}
|
|
@@ -360,22 +405,99 @@ export async function describeResources(
|
|
|
360
405
|
* axis for them to report against, the same way an out-of-band AWS child
|
|
361
406
|
* resource has none either.
|
|
362
407
|
*/
|
|
408
|
+
/** The declared GitOps CRs, indexed the way their controllers' labels can be
|
|
409
|
+
* resolved (#1549). `byRef` keys are `type\0namespace\0name` (Flux — exact);
|
|
410
|
+
* `argoByName` maps an Application's name to its declared entity, or null
|
|
411
|
+
* when two declared Applications share the name (ambiguous → no claim). */
|
|
412
|
+
interface GitopsIndex {
|
|
413
|
+
present: boolean;
|
|
414
|
+
byRef: Map<string, string>;
|
|
415
|
+
argoByName: Map<string, string | null>;
|
|
416
|
+
targetNamespaces: Set<string>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function gitopsIndex(declared: readonly Declared[]): GitopsIndex {
|
|
420
|
+
const byRef = new Map<string, string>();
|
|
421
|
+
const argoByName = new Map<string, string | null>();
|
|
422
|
+
const targetNamespaces = new Set<string>();
|
|
423
|
+
let present = false;
|
|
424
|
+
for (const { entityName, entityType, props } of declared) {
|
|
425
|
+
const isArgo = entityType === "K8s::Argo::Application";
|
|
426
|
+
const isFlux = entityType === "K8s::Flux::Kustomization" || entityType === "K8s::Flux::HelmRelease";
|
|
427
|
+
if (!isArgo && !isFlux) continue;
|
|
428
|
+
present = true;
|
|
429
|
+
const meta = props.metadata as { name?: string; namespace?: string } | undefined;
|
|
430
|
+
const spec = props.spec as { targetNamespace?: unknown; destination?: { namespace?: unknown } } | undefined;
|
|
431
|
+
const name = meta?.name;
|
|
432
|
+
if (!name) continue;
|
|
433
|
+
if (isFlux) {
|
|
434
|
+
byRef.set(`${entityType}\0${meta?.namespace ?? "default"}\0${name}`, entityName);
|
|
435
|
+
if (typeof spec?.targetNamespace === "string") targetNamespaces.add(spec.targetNamespace);
|
|
436
|
+
} else {
|
|
437
|
+
argoByName.set(name, argoByName.has(name) ? null : entityName);
|
|
438
|
+
if (typeof spec?.destination?.namespace === "string") targetNamespaces.add(spec.destination.namespace);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { present, byRef, argoByName, targetNamespaces };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The declared GitOps CR a swept object's controller labels resolve to, or
|
|
445
|
+
* undefined. Flux's label pairs are exact; Argo's bare instance label claims
|
|
446
|
+
* only a UNIQUE declared Application. */
|
|
447
|
+
function gitopsOwner(labels: Record<string, string> | undefined, gitops: GitopsIndex): string | undefined {
|
|
448
|
+
if (!labels || !gitops.present) return undefined;
|
|
449
|
+
const ksName = labels["kustomize.toolkit.fluxcd.io/name"];
|
|
450
|
+
const ksNs = labels["kustomize.toolkit.fluxcd.io/namespace"];
|
|
451
|
+
if (ksName && ksNs) {
|
|
452
|
+
const hit = gitops.byRef.get(`K8s::Flux::Kustomization\0${ksNs}\0${ksName}`);
|
|
453
|
+
if (hit) return hit;
|
|
454
|
+
}
|
|
455
|
+
const hrName = labels["helm.toolkit.fluxcd.io/name"];
|
|
456
|
+
const hrNs = labels["helm.toolkit.fluxcd.io/namespace"];
|
|
457
|
+
if (hrName && hrNs) {
|
|
458
|
+
const hit = gitops.byRef.get(`K8s::Flux::HelmRelease\0${hrNs}\0${hrName}`);
|
|
459
|
+
if (hit) return hit;
|
|
460
|
+
}
|
|
461
|
+
const instance = labels["app.kubernetes.io/instance"];
|
|
462
|
+
if (instance) {
|
|
463
|
+
const hit = gitops.argoByName.get(instance);
|
|
464
|
+
if (hit) return hit; // null (ambiguous) and undefined both decline
|
|
465
|
+
}
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
363
469
|
async function addRuntimeChildren(
|
|
364
470
|
client: K8sClient,
|
|
365
471
|
resources: Record<string, ResourceMetadata>,
|
|
366
472
|
owned: boolean | undefined,
|
|
367
473
|
groupVersions: ReadonlySet<string> = new Set(),
|
|
474
|
+
declared: readonly Declared[] = [],
|
|
368
475
|
): Promise<void> {
|
|
369
476
|
const declaredByUid = new Map<string, string>();
|
|
370
477
|
for (const [entityName, meta] of Object.entries(resources)) {
|
|
371
478
|
if (meta.physicalId) declaredByUid.set(meta.physicalId, entityName);
|
|
372
479
|
}
|
|
373
480
|
|
|
481
|
+
// GitOps attribution (#1549): Argo and Flux stamp their managed objects with
|
|
482
|
+
// labels, not ownerReferences, so the chain walk never reaches a declared
|
|
483
|
+
// entity for them. The label channel resolves against the DECLARED CRs by
|
|
484
|
+
// (type, namespace, name) — exact for Flux (its labels carry both halves) —
|
|
485
|
+
// and by unique name for Argo (its instance label carries no namespace, and
|
|
486
|
+
// it doubles as a generic Helm label, so anything ambiguous resolves to
|
|
487
|
+
// nothing: the module's standing conservatism).
|
|
488
|
+
const gitops = gitopsIndex(declared);
|
|
489
|
+
|
|
374
490
|
const namespaces = new Set<string>();
|
|
375
491
|
for (const meta of Object.values(resources)) {
|
|
376
492
|
const namespace = (meta.attributes as { namespace?: string } | undefined)?.namespace;
|
|
377
493
|
if (namespace) namespaces.add(namespace);
|
|
378
494
|
}
|
|
495
|
+
// The namespaces a declared GitOps CR TARGETS are declared literals too
|
|
496
|
+
// (Argo `spec.destination.namespace`, Flux `spec.targetNamespace`) — the
|
|
497
|
+
// controller puts the workloads there, so the scan must look there, and
|
|
498
|
+
// reading it off the declaration keeps this bounded by the estate, never a
|
|
499
|
+
// cluster-wide sweep.
|
|
500
|
+
for (const ns of gitops.targetNamespaces) namespaces.add(ns);
|
|
379
501
|
if (namespaces.size === 0) return;
|
|
380
502
|
|
|
381
503
|
// The kinds to scan: Pods, plus each declared groupVersion's namespaced,
|
|
@@ -384,6 +506,18 @@ async function addRuntimeChildren(
|
|
|
384
506
|
const kinds: Array<{ apiVersion: string; kind: string; typeName: string }> = [
|
|
385
507
|
{ apiVersion: "v1", kind: "Pod", typeName: "K8s::Core::Pod" },
|
|
386
508
|
];
|
|
509
|
+
// A GitOps estate's managed WORKLOADS live in groups the estate itself never
|
|
510
|
+
// declares (a pure Argo/Flux estate declares only the CRs), so without this
|
|
511
|
+
// the label channel could only ever fire on Pods (#1549).
|
|
512
|
+
if (gitops.present) {
|
|
513
|
+
kinds.push(
|
|
514
|
+
{ apiVersion: "apps/v1", kind: "Deployment", typeName: "K8s::Apps::Deployment" },
|
|
515
|
+
{ apiVersion: "apps/v1", kind: "StatefulSet", typeName: "K8s::Apps::StatefulSet" },
|
|
516
|
+
{ apiVersion: "apps/v1", kind: "DaemonSet", typeName: "K8s::Apps::DaemonSet" },
|
|
517
|
+
{ apiVersion: "v1", kind: "Service", typeName: "K8s::Core::Service" },
|
|
518
|
+
{ apiVersion: "v1", kind: "ConfigMap", typeName: "K8s::Core::ConfigMap" },
|
|
519
|
+
);
|
|
520
|
+
}
|
|
387
521
|
for (const gv of [...groupVersions].sort()) {
|
|
388
522
|
let infos;
|
|
389
523
|
try {
|
|
@@ -413,7 +547,13 @@ async function addRuntimeChildren(
|
|
|
413
547
|
if (!uid || !name || declaredByUid.has(uid)) return; // declared directly, or unaddressable
|
|
414
548
|
if (resources[`${namespace}/${name}`]) return; // already reported by an earlier kind
|
|
415
549
|
|
|
416
|
-
|
|
550
|
+
let ownerChain = await resolveK8sOwnerChain(obj, { declaredByUid, reader: client, namespace });
|
|
551
|
+
// The label channel (#1549) — only when the chain itself said nothing:
|
|
552
|
+
// a verdict the walk DID reach is never overridden.
|
|
553
|
+
if (ownerChain.root !== "declared") {
|
|
554
|
+
const entity = gitopsOwner(obj.metadata?.labels, gitops);
|
|
555
|
+
if (entity) ownerChain = { root: "declared", entity };
|
|
556
|
+
}
|
|
417
557
|
|
|
418
558
|
// `--owned`: withhold an object that is neither a runtime child of a
|
|
419
559
|
// declared entity nor carrying chant's own marker — the same rule the
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { k8sPlugin } from "./plugin";
|
|
|
9
9
|
// cfn-deploy.
|
|
10
10
|
export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./components/capability-plugin";
|
|
11
11
|
export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput } from "./components/kubectl-apply";
|
|
12
|
+
export { kustomizeApplyCapability, createKustomizeApplyCapability, type KustomizeApplyInput } from "./components/kustomize-apply";
|
|
12
13
|
|
|
13
14
|
// Default labels & annotations
|
|
14
15
|
export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels";
|
|
@@ -281,6 +281,28 @@ describe("kubectlApply", () => {
|
|
|
281
281
|
expect(body.metadata?.labels).toBeUndefined();
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
test("inline documents bypass the filesystem — the kustomize-apply seam (#1548)", async () => {
|
|
285
|
+
const cluster = fakeCluster({ respond: echoApplies });
|
|
286
|
+
const result = await applyManifest(
|
|
287
|
+
{
|
|
288
|
+
manifest: "kustomize:overlays/dev", // a label; nothing at this path
|
|
289
|
+
documents: [
|
|
290
|
+
{ apiVersion: "v1", kind: "Namespace", metadata: { name: "web" } },
|
|
291
|
+
{ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "app", namespace: "web" } },
|
|
292
|
+
],
|
|
293
|
+
stack: "web",
|
|
294
|
+
},
|
|
295
|
+
undefined,
|
|
296
|
+
cluster.connector,
|
|
297
|
+
);
|
|
298
|
+
expect(result.applied.map((r) => r.kind)).toEqual(["Namespace", "Deployment"]);
|
|
299
|
+
// The documents were stamped like any read-from-disk manifest.
|
|
300
|
+
const body = JSON.parse(String(cluster.layer.requests.find((r) => r.method === "PATCH")!.body)) as {
|
|
301
|
+
metadata?: { labels?: Record<string, string> };
|
|
302
|
+
};
|
|
303
|
+
expect(body.metadata?.labels?.["chant.intentius.io/stack"]).toBe("web");
|
|
304
|
+
});
|
|
305
|
+
|
|
284
306
|
test("an explicit context is honored and skips the environment lookup entirely", async () => {
|
|
285
307
|
const file = join(dir, "k8s.yaml");
|
|
286
308
|
writeFileSync(file, deploymentYaml);
|
|
@@ -55,8 +55,21 @@ import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types";
|
|
|
55
55
|
export type ApplyDeleteMode = "never" | "owned-only" | "gated";
|
|
56
56
|
|
|
57
57
|
export interface KubectlApplyArgs {
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Path to a manifest file, or a directory of them. With `documents` given,
|
|
60
|
+
* this becomes only the human-facing label the heartbeats and logs carry
|
|
61
|
+
* (e.g. `kustomize:<dir>`), and nothing is read from disk.
|
|
62
|
+
*/
|
|
59
63
|
manifest: string;
|
|
64
|
+
/**
|
|
65
|
+
* Already-parsed documents to apply INSTEAD of reading `manifest` (#1548):
|
|
66
|
+
* a renderer that produced the objects in memory — kustomize-apply's
|
|
67
|
+
* `kustomize build` — hands them straight in rather than round-tripping
|
|
68
|
+
* through a temp file. Everything downstream (ownership stamping, the
|
|
69
|
+
* marker-scoped prune, the self-conflict retake) is document-driven and
|
|
70
|
+
* behaves identically.
|
|
71
|
+
*/
|
|
72
|
+
documents?: K8sObject[];
|
|
60
73
|
/**
|
|
61
74
|
* kubectl context name. Uses the ambient context if omitted. To target the
|
|
62
75
|
* same cluster the read path (`describeResources`) resolved for an
|
|
@@ -285,7 +298,7 @@ export async function applyManifest(
|
|
|
285
298
|
signal?: AbortSignal,
|
|
286
299
|
connect: K8sConnector = defaultK8sConnector,
|
|
287
300
|
): Promise<ApplyManifestResult> {
|
|
288
|
-
const documents = readManifestDocuments(args.manifest);
|
|
301
|
+
const documents = args.documents ?? readManifestDocuments(args.manifest);
|
|
289
302
|
const { fieldManager, stack } = await resolveApplyIdentity(args);
|
|
290
303
|
const heartbeatInterval = setInterval(() => {
|
|
291
304
|
safeHeartbeat({ step: "kubectl apply", manifest: args.manifest });
|
|
@@ -49,6 +49,33 @@ describe("readiness model", () => {
|
|
|
49
49
|
expect(isReady(obj, DEFAULT_READINESS)).toBe(true);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
test("Flux overrides fail fast on Ready=False wedge reasons, wait on ordinary progress (#1549)", () => {
|
|
53
|
+
// A Flux wedge (bad build, failed upgrade, unreachable source) keeps
|
|
54
|
+
// Ready=False forever — before these entries the generic poll waited out
|
|
55
|
+
// its whole timeout. The ready half is still the kstatus default.
|
|
56
|
+
const spec = readinessFor("kustomize.toolkit.fluxcd.io", "Kustomization");
|
|
57
|
+
expect(spec).not.toBe(DEFAULT_READINESS);
|
|
58
|
+
expect(isReady(ready([{ type: "Ready", status: "True" }]), spec)).toBe(true);
|
|
59
|
+
|
|
60
|
+
const wedged = ready([{ type: "Ready", status: "False", reason: "BuildFailed" } as never]);
|
|
61
|
+
expect(isReady(wedged, spec)).toBe(false);
|
|
62
|
+
expect(firstTerminal(wedged, spec)).toBeDefined();
|
|
63
|
+
|
|
64
|
+
// Ready=False with a PROGRESS reason (a reconcile in flight) is not a
|
|
65
|
+
// wedge — keep polling.
|
|
66
|
+
const progressing = ready([{ type: "Ready", status: "False", reason: "Progressing" } as never]);
|
|
67
|
+
expect(firstTerminal(progressing, spec)).toBeUndefined();
|
|
68
|
+
|
|
69
|
+
// A reason on a TRUE condition is vocabulary, never terminal.
|
|
70
|
+
const healthyWithReason = ready([{ type: "Ready", status: "True", reason: "ReconciliationSucceeded" } as never]);
|
|
71
|
+
expect(firstTerminal(healthyWithReason, spec)).toBeUndefined();
|
|
72
|
+
|
|
73
|
+
const hr = readinessFor("helm.toolkit.fluxcd.io", "HelmRelease");
|
|
74
|
+
expect(firstTerminal(ready([{ type: "Ready", status: "False", reason: "UpgradeFailed" } as never]), hr)).toBeDefined();
|
|
75
|
+
const git = readinessFor("source.toolkit.fluxcd.io", "GitRepository");
|
|
76
|
+
expect(firstTerminal(ready([{ type: "Ready", status: "False", reason: "AuthenticationFailed" } as never]), git)).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
52
79
|
test("Argo override uses health/sync, not a Ready condition", () => {
|
|
53
80
|
const spec = readinessFor("argoproj.io", "Application");
|
|
54
81
|
expect(spec).not.toBe(DEFAULT_READINESS);
|
|
@@ -40,7 +40,23 @@ export interface PathMatch {
|
|
|
40
40
|
// With neither `equals` nor `oneOf`, holds when the value is present (non-null).
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Match a `status.conditions[]` entry by `type` AND its `reason` (#1549).
|
|
45
|
+
* The dot-path walk can't index into a conditions array, and the Flux
|
|
46
|
+
* toolkit's failure signal is exactly `conditions[type=Ready].reason` — a
|
|
47
|
+
* wedge like `BuildFailed` keeps `Ready=False` forever while the generic
|
|
48
|
+
* poll waits out its whole timeout. `status` defaults to "False": a reason
|
|
49
|
+
* on a True condition is progress vocabulary, not a wedge.
|
|
50
|
+
*/
|
|
51
|
+
export interface ConditionReasonMatch {
|
|
52
|
+
conditionType: string;
|
|
53
|
+
/** Required `status` of the condition (default "False"). */
|
|
54
|
+
status?: string;
|
|
55
|
+
/** Holds when the condition's `reason` is one of these. */
|
|
56
|
+
reasonOneOf: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ReadinessMatch = ConditionMatch | PathMatch | ConditionReasonMatch;
|
|
44
60
|
|
|
45
61
|
export interface ReadinessSpec {
|
|
46
62
|
/** All must hold for the resource to be ready. */
|
|
@@ -69,6 +85,19 @@ export const DEFAULT_READINESS: ReadinessSpec = {
|
|
|
69
85
|
* reports `health`/`sync`, not a `Ready` condition — the case that proves the
|
|
70
86
|
* override is necessary (#365).
|
|
71
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* Flux toolkit wedge reasons (#1549): a `Ready=False` with one of these
|
|
90
|
+
* `reason`s does not heal by waiting — a bad build, a failed install, an
|
|
91
|
+
* unreachable or invalid source. The toolkit is kstatus-conformant, so the
|
|
92
|
+
* READY half of every entry below is just the default; the value added is
|
|
93
|
+
* failing fast instead of polling out the full timeout.
|
|
94
|
+
*/
|
|
95
|
+
const FLUX_TERMINAL = (reasons: string[]): ReadinessSpec => ({
|
|
96
|
+
ready: [{ conditionType: "Ready", status: "True" }],
|
|
97
|
+
terminal: [{ conditionType: "Ready", reasonOneOf: reasons }],
|
|
98
|
+
observedGeneration: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
72
101
|
export const READINESS_OVERRIDES: Record<string, ReadinessSpec> = {
|
|
73
102
|
"argoproj.io/Application": {
|
|
74
103
|
ready: [
|
|
@@ -78,6 +107,38 @@ export const READINESS_OVERRIDES: Record<string, ReadinessSpec> = {
|
|
|
78
107
|
terminal: [{ path: "status.health.status", oneOf: ["Degraded", "Missing"] }],
|
|
79
108
|
observedGeneration: false,
|
|
80
109
|
},
|
|
110
|
+
"kustomize.toolkit.fluxcd.io/Kustomization": FLUX_TERMINAL([
|
|
111
|
+
"BuildFailed",
|
|
112
|
+
"HealthCheckFailed",
|
|
113
|
+
"ReconciliationFailed",
|
|
114
|
+
"ArtifactFailed",
|
|
115
|
+
"PruneFailed",
|
|
116
|
+
]),
|
|
117
|
+
"helm.toolkit.fluxcd.io/HelmRelease": FLUX_TERMINAL([
|
|
118
|
+
"InstallFailed",
|
|
119
|
+
"UpgradeFailed",
|
|
120
|
+
"RollbackFailed",
|
|
121
|
+
"UninstallFailed",
|
|
122
|
+
"ArtifactFailed",
|
|
123
|
+
"ChartPullError",
|
|
124
|
+
]),
|
|
125
|
+
"source.toolkit.fluxcd.io/GitRepository": FLUX_TERMINAL([
|
|
126
|
+
"AuthenticationFailed",
|
|
127
|
+
"GitOperationFailed",
|
|
128
|
+
"URLInvalid",
|
|
129
|
+
"StorageOperationFailed",
|
|
130
|
+
]),
|
|
131
|
+
"source.toolkit.fluxcd.io/OCIRepository": FLUX_TERMINAL([
|
|
132
|
+
"AuthenticationFailed",
|
|
133
|
+
"OCIPullFailed",
|
|
134
|
+
"URLInvalid",
|
|
135
|
+
"StorageOperationFailed",
|
|
136
|
+
]),
|
|
137
|
+
"source.toolkit.fluxcd.io/HelmChart": FLUX_TERMINAL([
|
|
138
|
+
"ChartPullError",
|
|
139
|
+
"ChartPackageError",
|
|
140
|
+
"StorageOperationFailed",
|
|
141
|
+
]),
|
|
81
142
|
};
|
|
82
143
|
|
|
83
144
|
/** Resolve the readiness spec for a resource: registry override, else default. */
|
|
@@ -94,15 +155,31 @@ function getPath(obj: unknown, path: string): unknown {
|
|
|
94
155
|
);
|
|
95
156
|
}
|
|
96
157
|
|
|
158
|
+
function isConditionReason(m: ReadinessMatch): m is ConditionReasonMatch {
|
|
159
|
+
return (m as ConditionReasonMatch).reasonOneOf !== undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
97
162
|
function isCondition(m: ReadinessMatch): m is ConditionMatch {
|
|
98
|
-
return (m as ConditionMatch).conditionType !== undefined;
|
|
163
|
+
return (m as ConditionMatch).conditionType !== undefined && !isConditionReason(m);
|
|
99
164
|
}
|
|
100
165
|
|
|
101
|
-
function
|
|
166
|
+
function findCondition(obj: unknown, conditionType: string): Record<string, unknown> | undefined {
|
|
102
167
|
const conds = getPath(obj, "status.conditions");
|
|
103
|
-
if (!Array.isArray(conds)) return
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
if (!Array.isArray(conds)) return undefined;
|
|
169
|
+
return conds.find((x) => x && typeof x === "object" && (x as Record<string, unknown>).type === conditionType) as
|
|
170
|
+
| Record<string, unknown>
|
|
171
|
+
| undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function matchCondition(obj: unknown, m: ConditionMatch): boolean {
|
|
175
|
+
const c = findCondition(obj, m.conditionType);
|
|
176
|
+
return !!c && String(c.status) === (m.status ?? "True");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function matchConditionReason(obj: unknown, m: ConditionReasonMatch): boolean {
|
|
180
|
+
const c = findCondition(obj, m.conditionType);
|
|
181
|
+
if (!c || String(c.status) !== (m.status ?? "False")) return false;
|
|
182
|
+
return typeof c.reason === "string" && m.reasonOneOf.includes(c.reason);
|
|
106
183
|
}
|
|
107
184
|
|
|
108
185
|
function matchPath(obj: unknown, m: PathMatch): boolean {
|
|
@@ -113,6 +190,7 @@ function matchPath(obj: unknown, m: PathMatch): boolean {
|
|
|
113
190
|
}
|
|
114
191
|
|
|
115
192
|
function matches(obj: unknown, m: ReadinessMatch): boolean {
|
|
193
|
+
if (isConditionReason(m)) return matchConditionReason(obj, m);
|
|
116
194
|
return isCondition(m) ? matchCondition(obj, m) : matchPath(obj, m);
|
|
117
195
|
}
|
|
118
196
|
|