@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.
@@ -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 is bounded to the kinds chant's own k8s surface deploys as
18
- * stack-scoped objects (workloads + the service/config plumbing around them).
19
- * A CRD-only unit is out of this bound and reports absent rather than null —
20
- * a real limit, stated here rather than papered over; widening the sweep to
21
- * discovery-driven kinds is a follow-up, not a silent claim.
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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAgCvE,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,CAuCxC"}
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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "0f71a63f04a8cd5caef3bd561c424d4dbbf973905a82ecb10189335ce638f844",
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": "a948db9f56e3d71f3e08460320bd55fbb98aa0223e8336ca4cac1d54da0223c8"
53
+ "composite": "da8f0e0aec605eff3c8ca4fca6c31f3bac6f8a2e9070e7708fd97cc49daf93a6"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.41.12",
3
+ "version": "0.41.14",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -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;;;;;;;;;;;;;;;GAeG;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,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACvF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAEpF,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,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"}
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.12",
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.12",
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
- /** A connector whose client answers list() from a canned map keyed by kind. */
6
- function fakeConnector(byKind: Record<string, unknown[]>, opts: { failKinds?: string[]; recordSelectors?: string[] } = {}): K8sConnector {
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 is bounded to the kinds chant's own k8s surface deploys as
18
- * stack-scoped objects (workloads + the service/config plumbing around them).
19
- * A CRD-only unit is out of this bound and reports absent rather than null —
20
- * a real limit, stated here rather than papered over; widening the sweep to
21
- * discovery-driven kinds is a follow-up, not a silent claim.
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
- for (const target of SWEEP_KINDS) {
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
- });
@@ -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
- }