@intentius/chant-lexicon-k8s 0.41.12 → 0.41.14
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/describe-stack-status.d.ts +12 -5
- package/dist/describe-stack-status.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +4 -3
- package/dist/op/activities/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/describe-stack-status.test.ts +98 -4
- package/src/describe-stack-status.ts +49 -6
- package/src/lint/post-synth/post-synth.test.ts +39 -0
- package/src/op/activities/index.ts +4 -4
- package/dist/op/activities/k3d.d.ts +0 -44
- package/dist/op/activities/k3d.d.ts.map +0 -1
- package/src/op/activities/k3d.test.ts +0 -68
- package/src/op/activities/k3d.ts +0 -96
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
* a caller observes with, and a mismatch would read a deployed unit as absent
|
|
15
15
|
* — the failure mode the tri-state exists to prevent.
|
|
16
16
|
*
|
|
17
|
-
* The sweep
|
|
18
|
-
* stack-scoped objects (workloads + the service/config plumbing around them)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* The sweep starts from the kinds chant's own k8s surface deploys as
|
|
18
|
+
* stack-scoped objects (workloads + the service/config plumbing around them),
|
|
19
|
+
* then widens to what the cluster itself serves: the CRDs (a unit that
|
|
20
|
+
* deploys definitions is a real unit — kubemicrovm-ops pins them
|
|
21
|
+
* `delete: never`), and every CRD-backed kind those definitions declare. A
|
|
22
|
+
* kubemicrovm workload stack is *entirely* custom resources, and under the
|
|
23
|
+
* fixed bound it read as absent while its VMs ran (#1528) — and because a
|
|
24
|
+
* component is live only when every unit is, that one false absence painted
|
|
25
|
+
* whole components dead. The CRD list is read unfiltered, since the unit
|
|
26
|
+
* under observation rarely owns the definitions its objects instantiate;
|
|
27
|
+
* each derived kind is then read with the same stack selector, so the cost
|
|
28
|
+
* is one label-filtered list per served CRD kind.
|
|
22
29
|
*/
|
|
23
30
|
import type { StackStatusObservation } from "@intentius/chant/lexicon";
|
|
24
31
|
import { type K8sConnector } from "./api/connect.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-stack-status.d.ts","sourceRoot":"","sources":["../src/describe-stack-status.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"describe-stack-status.d.ts","sourceRoot":"","sources":["../src/describe-stack-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmEvE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC/C,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwCxC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ba8a86870c372520c29075795e436c0d6b75bdfcf0efa31e659d02ade4a05d65",
|
|
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": "da8f0e0aec605eff3c8ca4fca6c31f3bac6f8a2e9070e7708fd97cc49daf93a6"
|
|
54
54
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* #1075, prune chant-owned objects it no longer declares; `applyManifest`
|
|
7
7
|
* is the same work with a report of what it did, which is what the
|
|
8
8
|
* Temporal lexicon's `nativeApply` dispatcher calls for a kubectl target)
|
|
9
|
-
* - k3dUp / k3dDown — boot/tear down a local k3d cluster
|
|
10
9
|
* - waitForArgoSync — block until an Argo CD Application is Healthy && Synced
|
|
11
10
|
*
|
|
11
|
+
* k3dUp / k3dDown moved again, to the k3d lexicon (chant #1410) — a lexicon
|
|
12
|
+
* owns its own product's activities, and k3d is its own product now. Projects
|
|
13
|
+
* using them list `k3d` in `lexicons`; loadActivities(["k3d"]) provides them.
|
|
14
|
+
*
|
|
12
15
|
* The step builders (kubectlApply, k3dUp, k3dDown) stay in core, re-exported from
|
|
13
16
|
* the temporal Op-authoring barrel like the other core builders. Each activity is
|
|
14
17
|
* dependency-light — it shells out to a CLI and does not import the k8s declarable
|
|
@@ -16,8 +19,6 @@
|
|
|
16
19
|
*/
|
|
17
20
|
export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl.js";
|
|
18
21
|
export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl.js";
|
|
19
|
-
export { k3dUp, k3dDown, k3dUpCommand, k3dDownCommand, k3dExistsCommand } from "./k3d.js";
|
|
20
|
-
export type { K3dUpArgs, K3dDownArgs } from "./k3d.js";
|
|
21
22
|
export { waitForArgoSync, defaultArgoStatusFetcher, ArgoSyncFailedError } from "./argo.js";
|
|
22
23
|
export type { WaitForArgoSyncArgs, ArgoAppStatus, ArgoStatusFetcher } from "./argo.js";
|
|
23
24
|
export { waitForReady, defaultResourceFetcher, apiResourceFetcher, ReadinessFailedError, readinessFor, isReady, firstTerminal, DEFAULT_READINESS, READINESS_OVERRIDES, } from "./wait-for-ready.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAC/E,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEpG,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,kBAAkB,EAClB,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"}
|
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.14",
|
|
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.14",
|
|
88
88
|
"typescript": "^5.9.3"
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -2,13 +2,23 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { describeStackStatus } from "./describe-stack-status";
|
|
3
3
|
import type { K8sConnector } from "./api/connect";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A connector whose client answers list() from a canned map keyed by kind.
|
|
7
|
+
* Selector-bearing lists model the server having filtered to the stack's
|
|
8
|
+
* objects (`byKind` holds what matched); the selector-less list is the CRD
|
|
9
|
+
* discovery read (#1528), answered from `discovery` so a test can serve
|
|
10
|
+
* definitions without them counting as the stack's own objects.
|
|
11
|
+
*/
|
|
12
|
+
function fakeConnector(
|
|
13
|
+
byKind: Record<string, unknown[]>,
|
|
14
|
+
opts: { failKinds?: string[]; recordSelectors?: string[]; discovery?: Record<string, unknown[]> } = {},
|
|
15
|
+
): K8sConnector {
|
|
7
16
|
return (async () => ({
|
|
8
17
|
client: {
|
|
9
18
|
async list(selector: { kind: string }, listOpts?: { labelSelector?: string }) {
|
|
10
|
-
if (listOpts?.labelSelector && opts.recordSelectors) opts.recordSelectors.push(listOpts.labelSelector);
|
|
11
19
|
if (opts.failKinds?.includes(selector.kind)) throw new Error("rbac denied");
|
|
20
|
+
if (!listOpts?.labelSelector) return opts.discovery?.[selector.kind] ?? [];
|
|
21
|
+
if (opts.recordSelectors) opts.recordSelectors.push(listOpts.labelSelector);
|
|
12
22
|
return byKind[selector.kind] ?? [];
|
|
13
23
|
},
|
|
14
24
|
},
|
|
@@ -23,6 +33,26 @@ const readyDeployment = {
|
|
|
23
33
|
const laggingDeployment = { ...readyDeployment, status: { replicas: 2, readyReplicas: 1 } };
|
|
24
34
|
const service = { kind: "Service", metadata: { name: "cc-api" } };
|
|
25
35
|
|
|
36
|
+
/** A served CRD, as the discovery read returns it — not the stack's object. */
|
|
37
|
+
const microvmCrd = {
|
|
38
|
+
kind: "CustomResourceDefinition",
|
|
39
|
+
metadata: { name: "microvms.lambda.aws.amazon.com" },
|
|
40
|
+
spec: {
|
|
41
|
+
group: "lambda.aws.amazon.com",
|
|
42
|
+
names: { kind: "MicroVM" },
|
|
43
|
+
versions: [{ name: "v1alpha1", served: true, storage: true }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const runningMicroVm = {
|
|
47
|
+
kind: "MicroVM",
|
|
48
|
+
metadata: { name: "kmv-vm", labels: { "chant.intentius.io/stack": "kmv-workload" } },
|
|
49
|
+
};
|
|
50
|
+
const microvmReplicaSet = {
|
|
51
|
+
kind: "MicroVMReplicaSet",
|
|
52
|
+
metadata: { name: "kmv-rs", labels: { "chant.intentius.io/stack": "kmv-workload" } },
|
|
53
|
+
status: { replicas: 2, readyReplicas: 2 },
|
|
54
|
+
};
|
|
55
|
+
|
|
26
56
|
describe("k8s describeStackStatus (#1495 piece 3)", () => {
|
|
27
57
|
test("selects on the labels chant's serializer stamps", async () => {
|
|
28
58
|
const selectors: string[] = [];
|
|
@@ -68,8 +98,72 @@ describe("k8s describeStackStatus (#1495 piece 3)", () => {
|
|
|
68
98
|
test("every read failing is indeterminate too", async () => {
|
|
69
99
|
const out = await describeStackStatus(
|
|
70
100
|
{ environment: "local", stack: "cc" },
|
|
71
|
-
fakeConnector({}, { failKinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret", "ServiceAccount", "Namespace", "Ingress", "PodDisruptionBudget", "HorizontalPodAutoscaler", "CronJob"] }),
|
|
101
|
+
fakeConnector({}, { failKinds: ["Deployment", "StatefulSet", "DaemonSet", "Service", "ConfigMap", "Secret", "ServiceAccount", "Namespace", "Ingress", "PodDisruptionBudget", "HorizontalPodAutoscaler", "CronJob", "CustomResourceDefinition"] }),
|
|
72
102
|
);
|
|
73
103
|
expect(out).toBeNull();
|
|
74
104
|
});
|
|
75
105
|
});
|
|
106
|
+
|
|
107
|
+
describe("the sweep reaches what the cluster serves, not only built-ins (#1528)", () => {
|
|
108
|
+
// A kubemicrovm estate made both gaps real: its crds unit deploys nothing
|
|
109
|
+
// but definitions, its workload unit nothing but custom resources, and
|
|
110
|
+
// under the fixed bound both read absent while the estate ran — which
|
|
111
|
+
// painted every component touching them dead in `components status --live`.
|
|
112
|
+
test("a unit that deploys the definitions themselves is present", async () => {
|
|
113
|
+
const crdWithLabel = {
|
|
114
|
+
...microvmCrd,
|
|
115
|
+
metadata: { ...microvmCrd.metadata, labels: { "chant.intentius.io/stack": "kmv-crds" } },
|
|
116
|
+
};
|
|
117
|
+
const out = await describeStackStatus(
|
|
118
|
+
{ environment: "local", stack: "kmv-crds" },
|
|
119
|
+
fakeConnector({ CustomResourceDefinition: [crdWithLabel] }),
|
|
120
|
+
);
|
|
121
|
+
expect(out).toMatchObject({ present: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a unit of custom resources is present via the kinds the CRDs declare", async () => {
|
|
125
|
+
const out = await describeStackStatus(
|
|
126
|
+
{ environment: "local", stack: "kmv-workload" },
|
|
127
|
+
fakeConnector({ MicroVM: [runningMicroVm] }, { discovery: { CustomResourceDefinition: [microvmCrd] } }),
|
|
128
|
+
);
|
|
129
|
+
expect(out).toMatchObject({ present: true });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a custom controller reporting replica readiness asserts health like a built-in", async () => {
|
|
133
|
+
const out = await describeStackStatus(
|
|
134
|
+
{ environment: "local", stack: "kmv-workload" },
|
|
135
|
+
fakeConnector(
|
|
136
|
+
{ MicroVMReplicaSet: [microvmReplicaSet] },
|
|
137
|
+
{
|
|
138
|
+
discovery: {
|
|
139
|
+
CustomResourceDefinition: [
|
|
140
|
+
{
|
|
141
|
+
...microvmCrd,
|
|
142
|
+
spec: { ...microvmCrd.spec, names: { kind: "MicroVMReplicaSet" } },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
expect(out).toMatchObject({ present: true, healthy: true, status: "1/1 workloads ready" });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("an unserved or malformed CRD derives nothing and breaks nothing", async () => {
|
|
153
|
+
const out = await describeStackStatus(
|
|
154
|
+
{ environment: "local", stack: "cc" },
|
|
155
|
+
fakeConnector(
|
|
156
|
+
{ Deployment: [readyDeployment] },
|
|
157
|
+
{
|
|
158
|
+
discovery: {
|
|
159
|
+
CustomResourceDefinition: [
|
|
160
|
+
{ kind: "CustomResourceDefinition", metadata: { name: "half" }, spec: { group: "x.io" } },
|
|
161
|
+
{ ...microvmCrd, spec: { ...microvmCrd.spec, versions: [{ name: "v1", served: false }] } },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
expect(out).toMatchObject({ present: true, healthy: true });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
* a caller observes with, and a mismatch would read a deployed unit as absent
|
|
15
15
|
* — the failure mode the tri-state exists to prevent.
|
|
16
16
|
*
|
|
17
|
-
* The sweep
|
|
18
|
-
* stack-scoped objects (workloads + the service/config plumbing around them)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* The sweep starts from the kinds chant's own k8s surface deploys as
|
|
18
|
+
* stack-scoped objects (workloads + the service/config plumbing around them),
|
|
19
|
+
* then widens to what the cluster itself serves: the CRDs (a unit that
|
|
20
|
+
* deploys definitions is a real unit — kubemicrovm-ops pins them
|
|
21
|
+
* `delete: never`), and every CRD-backed kind those definitions declare. A
|
|
22
|
+
* kubemicrovm workload stack is *entirely* custom resources, and under the
|
|
23
|
+
* fixed bound it read as absent while its VMs ran (#1528) — and because a
|
|
24
|
+
* component is live only when every unit is, that one false absence painted
|
|
25
|
+
* whole components dead. The CRD list is read unfiltered, since the unit
|
|
26
|
+
* under observation rarely owns the definitions its objects instantiate;
|
|
27
|
+
* each derived kind is then read with the same stack selector, so the cost
|
|
28
|
+
* is one label-filtered list per served CRD kind.
|
|
22
29
|
*/
|
|
23
30
|
import type { StackStatusObservation } from "@intentius/chant/lexicon";
|
|
24
31
|
import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
@@ -39,8 +46,43 @@ const SWEEP_KINDS: ReadonlyArray<{ apiVersion: string; kind: string }> = [
|
|
|
39
46
|
{ apiVersion: "policy/v1", kind: "PodDisruptionBudget" },
|
|
40
47
|
{ apiVersion: "autoscaling/v2", kind: "HorizontalPodAutoscaler" },
|
|
41
48
|
{ apiVersion: "batch/v1", kind: "CronJob" },
|
|
49
|
+
// The definitions themselves: a crds unit deploys these and nothing else.
|
|
50
|
+
{ apiVersion: "apiextensions.k8s.io/v1", kind: "CustomResourceDefinition" },
|
|
42
51
|
];
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Every kind the cluster's CRDs declare, one served version each (storage
|
|
55
|
+
* preferred). Read unfiltered — see the module doc. Failure to read the CRD
|
|
56
|
+
* list narrows the sweep back to the fixed bound rather than failing it.
|
|
57
|
+
*/
|
|
58
|
+
async function crdBackedKinds(client: {
|
|
59
|
+
list(target: { apiVersion: string; kind: string }, opts?: object): Promise<K8sObject[]>;
|
|
60
|
+
}): Promise<Array<{ apiVersion: string; kind: string }>> {
|
|
61
|
+
let crds: K8sObject[];
|
|
62
|
+
try {
|
|
63
|
+
crds = await client.list({ apiVersion: "apiextensions.k8s.io/v1", kind: "CustomResourceDefinition" });
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const kinds: Array<{ apiVersion: string; kind: string }> = [];
|
|
68
|
+
for (const crd of crds) {
|
|
69
|
+
const spec = (crd as {
|
|
70
|
+
spec?: {
|
|
71
|
+
group?: string;
|
|
72
|
+
names?: { kind?: string };
|
|
73
|
+
versions?: Array<{ name: string; served?: boolean; storage?: boolean }>;
|
|
74
|
+
};
|
|
75
|
+
}).spec;
|
|
76
|
+
const group = spec?.group;
|
|
77
|
+
const kind = spec?.names?.kind;
|
|
78
|
+
const versions = spec?.versions ?? [];
|
|
79
|
+
const version = versions.find((v) => v.storage && v.served) ?? versions.find((v) => v.served);
|
|
80
|
+
if (!group || !kind || !version) continue;
|
|
81
|
+
kinds.push({ apiVersion: `${group}/${version.name}`, kind });
|
|
82
|
+
}
|
|
83
|
+
return kinds;
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
/** A workload is healthy when its controller reports every replica ready; a
|
|
45
87
|
* kind with no readiness story (a ConfigMap, a Service) asserts nothing. */
|
|
46
88
|
function objectHealthy(obj: K8sObject): boolean | undefined {
|
|
@@ -74,7 +116,8 @@ export async function describeStackStatus(
|
|
|
74
116
|
|
|
75
117
|
const matched: K8sObject[] = [];
|
|
76
118
|
let anyRead = false;
|
|
77
|
-
|
|
119
|
+
const targets = [...SWEEP_KINDS, ...(await crdBackedKinds(client))];
|
|
120
|
+
for (const target of targets) {
|
|
78
121
|
try {
|
|
79
122
|
const items = await client.list(target, { labelSelector: selector });
|
|
80
123
|
anyRead = true;
|
|
@@ -667,6 +667,45 @@ describe("WK8204: runAsNonRoot", () => {
|
|
|
667
667
|
expect(diags.length).toBe(0);
|
|
668
668
|
});
|
|
669
669
|
|
|
670
|
+
// #1482 — the manifest arrives as real YAML, and the initContainer's args is
|
|
671
|
+
// a block scalar. The parser bug turned `- |` into the string "|" and hoisted
|
|
672
|
+
// every key after it (securityContext, the containers list itself) to the
|
|
673
|
+
// document root, so this check warned on a compliant initContainer and never
|
|
674
|
+
// saw the app container at all.
|
|
675
|
+
test("initContainer with a block-scalar arg keeps its securityContext, and the app container is still checked (#1482)", () => {
|
|
676
|
+
const yaml = [
|
|
677
|
+
"apiVersion: apps/v1",
|
|
678
|
+
"kind: Deployment",
|
|
679
|
+
"metadata:",
|
|
680
|
+
" name: app",
|
|
681
|
+
"spec:",
|
|
682
|
+
" template:",
|
|
683
|
+
" spec:",
|
|
684
|
+
" initContainers:",
|
|
685
|
+
" - name: wait",
|
|
686
|
+
" image: pg:16",
|
|
687
|
+
" args:",
|
|
688
|
+
" - |",
|
|
689
|
+
" set -eu",
|
|
690
|
+
" echo ready",
|
|
691
|
+
" securityContext:",
|
|
692
|
+
" runAsNonRoot: true",
|
|
693
|
+
" runAsUser: 1001",
|
|
694
|
+
" containers:",
|
|
695
|
+
" - name: app",
|
|
696
|
+
" image: app:1.0",
|
|
697
|
+
" securityContext: {}",
|
|
698
|
+
"",
|
|
699
|
+
].join("\n");
|
|
700
|
+
const ctx = makeCtx(yaml);
|
|
701
|
+
const diags = wk8204.check(ctx);
|
|
702
|
+
// The compliant initContainer produces nothing; the bare app container is
|
|
703
|
+
// the one — and the only one — that warns.
|
|
704
|
+
expect(diags.length).toBe(1);
|
|
705
|
+
expect(diags[0].message).toContain('"app"');
|
|
706
|
+
expect(diags[0].message).not.toContain("wait");
|
|
707
|
+
});
|
|
708
|
+
|
|
670
709
|
test("pod-level runAsUser satisfies container-level runAsNonRoot", () => {
|
|
671
710
|
const ctx = makeCtx(JSON.stringify({
|
|
672
711
|
apiVersion: "apps/v1",
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* #1075, prune chant-owned objects it no longer declares; `applyManifest`
|
|
7
7
|
* is the same work with a report of what it did, which is what the
|
|
8
8
|
* Temporal lexicon's `nativeApply` dispatcher calls for a kubectl target)
|
|
9
|
-
* - k3dUp / k3dDown — boot/tear down a local k3d cluster
|
|
10
9
|
* - waitForArgoSync — block until an Argo CD Application is Healthy && Synced
|
|
11
10
|
*
|
|
11
|
+
* k3dUp / k3dDown moved again, to the k3d lexicon (chant #1410) — a lexicon
|
|
12
|
+
* owns its own product's activities, and k3d is its own product now. Projects
|
|
13
|
+
* using them list `k3d` in `lexicons`; loadActivities(["k3d"]) provides them.
|
|
14
|
+
*
|
|
12
15
|
* The step builders (kubectlApply, k3dUp, k3dDown) stay in core, re-exported from
|
|
13
16
|
* the temporal Op-authoring barrel like the other core builders. Each activity is
|
|
14
17
|
* dependency-light — it shells out to a CLI and does not import the k8s declarable
|
|
@@ -17,9 +20,6 @@
|
|
|
17
20
|
export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl";
|
|
18
21
|
export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl";
|
|
19
22
|
|
|
20
|
-
export { k3dUp, k3dDown, k3dUpCommand, k3dDownCommand, k3dExistsCommand } from "./k3d";
|
|
21
|
-
export type { K3dUpArgs, K3dDownArgs } from "./k3d";
|
|
22
|
-
|
|
23
23
|
export { waitForArgoSync, defaultArgoStatusFetcher, ArgoSyncFailedError } from "./argo";
|
|
24
24
|
export type { WaitForArgoSyncArgs, ArgoAppStatus, ArgoStatusFetcher } from "./argo";
|
|
25
25
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export interface K3dUpArgs {
|
|
2
|
-
/** Cluster name (`k3d cluster create <name>`). */
|
|
3
|
-
name: string;
|
|
4
|
-
/** Number of server (control-plane) nodes. */
|
|
5
|
-
servers?: number;
|
|
6
|
-
/** Number of agent (worker) nodes. */
|
|
7
|
-
agents?: number;
|
|
8
|
-
/** k3s image, e.g. `rancher/k3s:v1.31.4-k3s1`. */
|
|
9
|
-
image?: string;
|
|
10
|
-
/** Port mappings, e.g. `["8080:80@loadbalancer"]`. */
|
|
11
|
-
ports?: string[];
|
|
12
|
-
/** Create a managed local registry with this name (`--registry-create`). */
|
|
13
|
-
registryCreate?: string;
|
|
14
|
-
/** Path to a k3d config file (`--config`). */
|
|
15
|
-
configFile?: string;
|
|
16
|
-
/** Readiness timeout for `--wait`. Default: `120s`. */
|
|
17
|
-
timeout?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface K3dDownArgs {
|
|
20
|
-
/** Cluster name to delete. */
|
|
21
|
-
name: string;
|
|
22
|
-
}
|
|
23
|
-
/** `k3d cluster list <name> --no-headers` — non-empty stdout means the cluster exists. */
|
|
24
|
-
export declare function k3dExistsCommand(name: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Build the `k3d cluster create` command. k3d merges the new cluster into the
|
|
27
|
-
* default kubeconfig and switches context by default, so no extra flags are
|
|
28
|
-
* needed for the manifests to be `kubectl apply`-able afterward.
|
|
29
|
-
*/
|
|
30
|
-
export declare function k3dUpCommand(args: K3dUpArgs): string;
|
|
31
|
-
/** Build the `k3d cluster delete` command. */
|
|
32
|
-
export declare function k3dDownCommand(args: K3dDownArgs): string;
|
|
33
|
-
/**
|
|
34
|
-
* Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: if a
|
|
35
|
-
* cluster of the same name already exists it is left as-is. Uses longInfra
|
|
36
|
-
* profile — 20m timeout, heartbeat every 15s (creation may pull the k3s image).
|
|
37
|
-
*/
|
|
38
|
-
export declare function k3dUp(args: K3dUpArgs, signal?: AbortSignal): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Delete a local k3d cluster. Uses fastIdempotent profile — 5m timeout.
|
|
41
|
-
* `k3d cluster delete` is a no-op success when the cluster is already gone.
|
|
42
|
-
*/
|
|
43
|
-
export declare function k3dDown(args: K3dDownArgs, signal?: AbortSignal): Promise<void>;
|
|
44
|
-
//# sourceMappingURL=k3d.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"k3d.d.ts","sourceRoot":"","sources":["../../../src/op/activities/k3d.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAWpD;AAED,8CAA8C;AAC9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAExD;AAED;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBhF;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpF"}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { k3dUpCommand, k3dDownCommand, k3dExistsCommand } from "./k3d";
|
|
3
|
-
|
|
4
|
-
describe("k3dUpCommand (#704)", () => {
|
|
5
|
-
test("minimal — name only, defaults to --wait with a 120s timeout", () => {
|
|
6
|
-
const cmd = k3dUpCommand({ name: "chant-local" });
|
|
7
|
-
expect(cmd).toBe("k3d cluster create chant-local --wait --timeout 120s");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("servers/agents/image flags", () => {
|
|
11
|
-
const cmd = k3dUpCommand({
|
|
12
|
-
name: "dev",
|
|
13
|
-
servers: 1,
|
|
14
|
-
agents: 2,
|
|
15
|
-
image: "rancher/k3s:v1.31.4-k3s1",
|
|
16
|
-
});
|
|
17
|
-
expect(cmd).toContain("--servers 1");
|
|
18
|
-
expect(cmd).toContain("--agents 2");
|
|
19
|
-
expect(cmd).toContain("--image rancher/k3s:v1.31.4-k3s1");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("each port becomes a quoted -p flag", () => {
|
|
23
|
-
const cmd = k3dUpCommand({
|
|
24
|
-
name: "dev",
|
|
25
|
-
ports: ["8080:80@loadbalancer", "8443:443@loadbalancer"],
|
|
26
|
-
});
|
|
27
|
-
expect(cmd).toContain('-p "8080:80@loadbalancer"');
|
|
28
|
-
expect(cmd).toContain('-p "8443:443@loadbalancer"');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("registry and config file", () => {
|
|
32
|
-
const cmd = k3dUpCommand({
|
|
33
|
-
name: "dev",
|
|
34
|
-
registryCreate: "chant-registry",
|
|
35
|
-
configFile: "k3d.yaml",
|
|
36
|
-
});
|
|
37
|
-
expect(cmd).toContain("--registry-create chant-registry");
|
|
38
|
-
expect(cmd).toContain("--config k3d.yaml");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("custom timeout overrides the default", () => {
|
|
42
|
-
const cmd = k3dUpCommand({ name: "dev", timeout: "300s" });
|
|
43
|
-
expect(cmd).toContain("--timeout 300s");
|
|
44
|
-
expect(cmd).not.toContain("120s");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("zero servers is emitted (not dropped as falsy)", () => {
|
|
48
|
-
// servers: 0 is unusual but must round-trip — guarded by `!== undefined`.
|
|
49
|
-
const cmd = k3dUpCommand({ name: "dev", servers: 0 });
|
|
50
|
-
expect(cmd).toContain("--servers 0");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("k3dDownCommand (#704)", () => {
|
|
55
|
-
test("deletes the named cluster", () => {
|
|
56
|
-
expect(k3dDownCommand({ name: "chant-local" })).toBe(
|
|
57
|
-
"k3d cluster delete chant-local",
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("k3dExistsCommand (#704)", () => {
|
|
63
|
-
test("lists a single cluster with no headers for an emptiness check", () => {
|
|
64
|
-
expect(k3dExistsCommand("chant-local")).toBe(
|
|
65
|
-
"k3d cluster list chant-local --no-headers",
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
});
|
package/src/op/activities/k3d.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { safeHeartbeat } from "@intentius/chant/op";
|
|
4
|
-
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
|
-
|
|
7
|
-
export interface K3dUpArgs {
|
|
8
|
-
/** Cluster name (`k3d cluster create <name>`). */
|
|
9
|
-
name: string;
|
|
10
|
-
/** Number of server (control-plane) nodes. */
|
|
11
|
-
servers?: number;
|
|
12
|
-
/** Number of agent (worker) nodes. */
|
|
13
|
-
agents?: number;
|
|
14
|
-
/** k3s image, e.g. `rancher/k3s:v1.31.4-k3s1`. */
|
|
15
|
-
image?: string;
|
|
16
|
-
/** Port mappings, e.g. `["8080:80@loadbalancer"]`. */
|
|
17
|
-
ports?: string[];
|
|
18
|
-
/** Create a managed local registry with this name (`--registry-create`). */
|
|
19
|
-
registryCreate?: string;
|
|
20
|
-
/** Path to a k3d config file (`--config`). */
|
|
21
|
-
configFile?: string;
|
|
22
|
-
/** Readiness timeout for `--wait`. Default: `120s`. */
|
|
23
|
-
timeout?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface K3dDownArgs {
|
|
27
|
-
/** Cluster name to delete. */
|
|
28
|
-
name: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** `k3d cluster list <name> --no-headers` — non-empty stdout means the cluster exists. */
|
|
32
|
-
export function k3dExistsCommand(name: string): string {
|
|
33
|
-
return `k3d cluster list ${name} --no-headers`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build the `k3d cluster create` command. k3d merges the new cluster into the
|
|
38
|
-
* default kubeconfig and switches context by default, so no extra flags are
|
|
39
|
-
* needed for the manifests to be `kubectl apply`-able afterward.
|
|
40
|
-
*/
|
|
41
|
-
export function k3dUpCommand(args: K3dUpArgs): string {
|
|
42
|
-
const parts = ["k3d", "cluster", "create", args.name];
|
|
43
|
-
if (args.servers !== undefined) parts.push(`--servers ${args.servers}`);
|
|
44
|
-
if (args.agents !== undefined) parts.push(`--agents ${args.agents}`);
|
|
45
|
-
if (args.image) parts.push(`--image ${args.image}`);
|
|
46
|
-
for (const p of args.ports ?? []) parts.push(`-p "${p}"`);
|
|
47
|
-
if (args.registryCreate) parts.push(`--registry-create ${args.registryCreate}`);
|
|
48
|
-
if (args.configFile) parts.push(`--config ${args.configFile}`);
|
|
49
|
-
parts.push("--wait");
|
|
50
|
-
parts.push(`--timeout ${args.timeout ?? "120s"}`);
|
|
51
|
-
return parts.join(" ");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Build the `k3d cluster delete` command. */
|
|
55
|
-
export function k3dDownCommand(args: K3dDownArgs): string {
|
|
56
|
-
return `k3d cluster delete ${args.name}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: if a
|
|
61
|
-
* cluster of the same name already exists it is left as-is. Uses longInfra
|
|
62
|
-
* profile — 20m timeout, heartbeat every 15s (creation may pull the k3s image).
|
|
63
|
-
*/
|
|
64
|
-
export async function k3dUp(args: K3dUpArgs, signal?: AbortSignal): Promise<void> {
|
|
65
|
-
try {
|
|
66
|
-
const { stdout } = await execAsync(k3dExistsCommand(args.name), { signal });
|
|
67
|
-
if (stdout.trim()) {
|
|
68
|
-
console.log(`k3d cluster "${args.name}" already exists — skipping create`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
// `cluster list` errors when the cluster is absent — fall through to create.
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const heartbeatInterval = setInterval(() => {
|
|
76
|
-
safeHeartbeat({ step: "k3d cluster create", cluster: args.name });
|
|
77
|
-
}, 15_000);
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const { stdout, stderr } = await execAsync(k3dUpCommand(args), { signal });
|
|
81
|
-
if (stdout) console.log(stdout);
|
|
82
|
-
if (stderr) console.error(stderr);
|
|
83
|
-
} finally {
|
|
84
|
-
clearInterval(heartbeatInterval);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Delete a local k3d cluster. Uses fastIdempotent profile — 5m timeout.
|
|
90
|
-
* `k3d cluster delete` is a no-op success when the cluster is already gone.
|
|
91
|
-
*/
|
|
92
|
-
export async function k3dDown(args: K3dDownArgs, signal?: AbortSignal): Promise<void> {
|
|
93
|
-
const { stdout, stderr } = await execAsync(k3dDownCommand(args), { signal });
|
|
94
|
-
if (stdout) console.log(stdout);
|
|
95
|
-
if (stderr) console.error(stderr);
|
|
96
|
-
}
|