@intentius/chant-lexicon-k8s 0.124.31 → 0.124.33

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.
Files changed (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-k8s",
3
- "version": "0.124.31",
3
+ "version": "0.124.33",
4
4
  "description": "Kubernetes lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -32,32 +32,32 @@
32
32
  ".": {
33
33
  "development": "./src/index.ts",
34
34
  "types": "./dist/index.d.ts",
35
- "default": "./src/index.ts"
35
+ "default": "./dist/index.js"
36
36
  },
37
37
  "./*": {
38
38
  "development": "./src/*.ts",
39
39
  "types": "./dist/*.d.ts",
40
- "default": "./src/*.ts"
40
+ "default": "./dist/*.js"
41
41
  },
42
42
  "./detect": {
43
43
  "development": "./src/detect.ts",
44
44
  "types": "./dist/detect.d.ts",
45
- "default": "./src/detect.ts"
45
+ "default": "./dist/detect.js"
46
46
  },
47
47
  "./components": {
48
48
  "development": "./src/components/index.ts",
49
49
  "types": "./dist/components/index.d.ts",
50
- "default": "./src/components/index.ts"
50
+ "default": "./dist/components/index.js"
51
51
  },
52
52
  "./lint/post-synth": {
53
53
  "development": "./src/lint/post-synth/index.ts",
54
54
  "types": "./dist/lint/post-synth/index.d.ts",
55
- "default": "./src/lint/post-synth/index.ts"
55
+ "default": "./dist/lint/post-synth/index.js"
56
56
  },
57
57
  "./op/activities": {
58
58
  "development": "./src/op/activities/index.ts",
59
59
  "types": "./dist/op/activities/index.d.ts",
60
- "default": "./src/op/activities/index.ts"
60
+ "default": "./dist/op/activities/index.js"
61
61
  },
62
62
  "./manifest": "./dist/manifest.json",
63
63
  "./meta": "./dist/meta.json",
@@ -69,16 +69,16 @@
69
69
  "validate": "tsx src/validate-cli.ts",
70
70
  "docs": "tsx src/codegen/docs-cli.ts",
71
71
  "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
72
- "build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete && cp src/generated/index.d.ts dist/generated/index.d.d.ts"
72
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json && cp src/generated/index.d.ts dist/generated/index.d.d.ts"
73
73
  },
74
74
  "dependencies": {
75
- "@intentius/chant-lexicon-otel": "^0.124.31",
76
- "@intentius/chant-lexicon-prometheus": "^0.124.31",
75
+ "@intentius/chant-lexicon-otel": "^0.124.33",
76
+ "@intentius/chant-lexicon-prometheus": "^0.124.33",
77
77
  "js-yaml": "^4.3.1",
78
78
  "@types/js-yaml": "^4.0.9"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@intentius/chant-k8s-client": "^0.124.31"
81
+ "@intentius/chant-k8s-client": "^0.124.33"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@intentius/chant": "*",
@@ -86,6 +86,6 @@
86
86
  },
87
87
  "peerDependencies": {
88
88
  "zod": "^4.3.6",
89
- "@intentius/chant": "^0.124.31"
89
+ "@intentius/chant": "^0.124.33"
90
90
  }
91
91
  }
@@ -13,8 +13,9 @@
13
13
  */
14
14
 
15
15
  import { createRequire } from "module";
16
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
16
17
 
17
- const require = createRequire(import.meta.url);
18
+ const require = createRequire(moduleSourceUrl(import.meta.url));
18
19
 
19
20
  /** One resource's addressing information, as `chant generate` emits it. */
20
21
  export interface K8sOperationDescriptor {
@@ -6,7 +6,7 @@
6
6
  import { readFileSync } from "fs";
7
7
  import { k8sPlugin } from "../plugin";
8
8
  import { join, dirname } from "path";
9
- import { fileURLToPath } from "url";
9
+ import { moduleSourceDir } from "@intentius/chant/module-source";
10
10
  import {
11
11
  packagePipeline,
12
12
  collectSkills,
@@ -17,7 +17,7 @@ import { generate } from "./generate";
17
17
 
18
18
  export type { PackageOptions, PackageResult };
19
19
 
20
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const pkgDir = dirname(moduleSourceDir(import.meta.url));
21
21
 
22
22
  /**
23
23
  * Package the Kubernetes lexicon into a distributable BundleSpec.
@@ -16,8 +16,11 @@ export * from "./builders";
16
16
  export {
17
17
  kubectlApplyCapability,
18
18
  createKubectlApplyCapability,
19
+ KubectlApplyPlanMovedError,
19
20
  type KubectlApplyInput,
20
21
  type KubectlApplyOutcome,
22
+ type KubectlApplyDryRun,
23
+ type KubectlApplyPlanArtifact,
21
24
  } from "./kubectl-apply";
22
25
  export {
23
26
  kustomizeApplyCapability,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `kubectl-apply` plan against a real API server (#3998).
3
+ *
4
+ * Skipped unless `CHANT_K3D_CONTEXT` names a kubeconfig context, such as
5
+ * the one `k3d cluster create chant-e2e` writes (`k3d-chant-e2e`). Each run
6
+ * works in a namespace of its own and deletes it afterwards.
7
+ *
8
+ * What only a real server shows: the defaults it fills in on a dry run match
9
+ * the live object's, so applying a manifest and planning it again plans no
10
+ * change.
11
+ */
12
+
13
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
14
+ import { execFileSync } from "node:child_process";
15
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { dump } from "js-yaml";
19
+ import { createKubectlApplyCapability, KubectlApplyPlanMovedError, type KubectlApplyInput } from "./kubectl-apply";
20
+
21
+ const context = process.env.CHANT_K3D_CONTEXT;
22
+ const suffix = Math.random().toString(36).slice(2, 8);
23
+ const ns = `chant-plan-${suffix}`;
24
+ const stack = `plan-e2e-${suffix}`;
25
+
26
+ const namespace = { apiVersion: "v1", kind: "Namespace", metadata: { name: ns } };
27
+ const config = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "cfg", namespace: ns }, data: { a: "1" } };
28
+ const deployment = (replicas: number, app = "web") => ({
29
+ apiVersion: "apps/v1",
30
+ kind: "Deployment",
31
+ metadata: { name: "web", namespace: ns },
32
+ spec: {
33
+ replicas,
34
+ selector: { matchLabels: { app } },
35
+ template: {
36
+ metadata: { labels: { app } },
37
+ spec: { containers: [{ name: "pause", image: "registry.k8s.io/pause:3.9" }] },
38
+ },
39
+ },
40
+ });
41
+
42
+ describe.skipIf(!context)("kubectl-apply plan on k3d (#3998)", () => {
43
+ let dir: string;
44
+ const cap = createKubectlApplyCapability();
45
+ const ctx = { env: "e2e", component: "web" };
46
+ const write = (documents: object[]): string => {
47
+ const file = join(dir, `k8s-${Math.random().toString(36).slice(2)}.yaml`);
48
+ writeFileSync(file, documents.map((d) => dump(d)).join("---\n"));
49
+ return file;
50
+ };
51
+ const input = (documents: object[], extra: Partial<KubectlApplyInput> = {}): KubectlApplyInput => ({
52
+ manifest: write(documents),
53
+ stack,
54
+ context: context!,
55
+ ...extra,
56
+ });
57
+ const actions = (entries: Array<{ address: string; action: string }>) =>
58
+ Object.fromEntries(entries.map((e) => [e.address, e.action]));
59
+
60
+ beforeAll(() => {
61
+ dir = mkdtempSync(join(tmpdir(), "chant-3998-"));
62
+ });
63
+ afterAll(() => {
64
+ rmSync(dir, { recursive: true, force: true });
65
+ execFileSync("kubectl", ["--context", context!, "delete", "namespace", ns, "--wait=false", "--ignore-not-found"]);
66
+ });
67
+
68
+ test("a new manifest plans creates, the namespace's objects included, and run applies the plan", async () => {
69
+ const step = input([namespace, config, deployment(1)]);
70
+ const planned = await cap.plan!(ctx, step);
71
+ expect(planned.member).toBe(stack);
72
+ expect(actions(planned.changeSet!.entries)).toEqual({
73
+ [`Namespace/${ns}`]: "create",
74
+ [`ConfigMap/${ns}/cfg`]: "create",
75
+ [`Deployment/${ns}/web`]: "create",
76
+ });
77
+ const applied = await cap.run({ ...ctx, plans: { [stack]: planned.artifact } }, step);
78
+ expect(applied.applied.map((r) => r.kind)).toEqual(["Namespace", "ConfigMap", "Deployment"]);
79
+ }, 60_000);
80
+
81
+ test("the same manifest planned again is no change: server defaults and bookkeeping drop out", async () => {
82
+ const planned = await cap.plan!(ctx, input([namespace, config, deployment(1)]));
83
+ expect(planned.changeSet!.entries.map((e) => [e.address, e.action, e.attributes])).toEqual([
84
+ [`Namespace/${ns}`, "no-op", []],
85
+ [`ConfigMap/${ns}/cfg`, "no-op", []],
86
+ [`Deployment/${ns}/web`, "no-op", []],
87
+ ]);
88
+ }, 60_000);
89
+
90
+ test("a changed field is an update naming it, and owned-only plans the removed object's delete", async () => {
91
+ const planned = await cap.plan!(ctx, input([namespace, deployment(2)], { delete: "owned-only" }));
92
+ const byAddress = Object.fromEntries(planned.changeSet!.entries.map((e) => [e.address, e]));
93
+ expect(byAddress[`Deployment/${ns}/web`]).toMatchObject({
94
+ action: "update",
95
+ disruption: "in-place",
96
+ attributes: [{ path: "spec.replicas", before: 1, after: 2 }],
97
+ });
98
+ expect(byAddress[`ConfigMap/${ns}/cfg`]).toMatchObject({ action: "delete", disruption: "destroy" });
99
+ expect(byAddress[`Namespace/${ns}`]).toMatchObject({ action: "no-op" });
100
+ }, 60_000);
101
+
102
+ test("run refuses an approved plan once the cluster moved, and applies nothing", async () => {
103
+ // Approved while nothing undeclared is owned: no deletes.
104
+ const approved = await cap.plan!(ctx, input([namespace, config, deployment(2)], { delete: "owned-only" }));
105
+ expect(approved.changeSet!.entries.some((e) => e.action === "delete")).toBe(false);
106
+ // Then another apply under the same stack adds an owned object the approved manifest does not declare.
107
+ const extra = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "extra", namespace: ns }, data: { b: "2" } };
108
+ await cap.run(ctx, input([namespace, config, extra, deployment(1)]));
109
+ const moved = cap.run(
110
+ { ...ctx, plans: { [stack]: approved.artifact } },
111
+ input([namespace, config, deployment(2)], { delete: "owned-only" }),
112
+ );
113
+ await expect(moved).rejects.toBeInstanceOf(KubectlApplyPlanMovedError);
114
+ const kubectl = (...args: string[]) => String(execFileSync("kubectl", ["--context", context!, "-n", ns, ...args]));
115
+ expect(kubectl("get", "configmap", "extra", "-o", "jsonpath={.data.b}")).toBe("2");
116
+ expect(kubectl("get", "deployment", "web", "-o", "jsonpath={.spec.replicas}")).toBe("1");
117
+ }, 60_000);
118
+
119
+ test("an immutable selector change is an update with a hole saying the apply would fail", async () => {
120
+ const planned = await cap.plan!(ctx, input([namespace, config, deployment(1, "web2")]));
121
+ const entry = planned.changeSet!.entries.find((e) => e.address === `Deployment/${ns}/web`);
122
+ expect(entry).toMatchObject({ action: "update", disruption: "unknown" });
123
+ expect(planned.changeSet!.member.holes).toEqual([
124
+ expect.objectContaining({ address: `Deployment/${ns}/web`, reason: expect.stringMatching(/immutable/) }),
125
+ ]);
126
+ }, 60_000);
127
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { createKubectlApplyCapability, k8sCapabilityPlugin } from "../index";
3
3
  import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
4
+ import { isPlanDigest } from "@intentius/chant/lifecycle/plan-digest";
4
5
 
5
6
  describe("kubectl-apply capability (#1495 piece 2)", () => {
6
7
  test("the plugin satisfies the CapabilityPlugin contract and registers the verb", () => {
@@ -126,3 +127,264 @@ describe("kubectl-apply with a release (#3061, ws-081)", () => {
126
127
  }
127
128
  });
128
129
  });
130
+
131
+ describe("kubectl-apply plan: a server-side dry-run diff (#3998)", () => {
132
+ type DryRun = import("./kubectl-apply").KubectlApplyDryRun;
133
+ const web = { apiVersion: "apps/v1", kind: "Deployment", name: "web", namespace: "prod" };
134
+ const deployment = () => ({ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "web", namespace: "prod" }, spec: { replicas: 3 } });
135
+ const unchanged = (): DryRun => ({ fieldManager: "chant:web", stack: "web", objects: [{ ref: web, action: "no-op", changes: [] }], deletes: [], retained: [] });
136
+ const scaled = (to: number): DryRun => ({
137
+ fieldManager: "chant:web",
138
+ stack: "web",
139
+ objects: [{ ref: web, action: "update", changes: [{ path: "spec.replicas", before: 3, after: to }] }],
140
+ deletes: [],
141
+ retained: [],
142
+ });
143
+ const noApply = async (): Promise<never> => {
144
+ throw new Error("applied");
145
+ };
146
+
147
+ test("plan dry-runs the documents run would apply and answers the stack as the member", async () => {
148
+ let seen: Record<string, unknown> = {};
149
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async (args) => {
150
+ seen = args as never;
151
+ return scaled(5);
152
+ });
153
+ const planned = await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml", stack: "web", delete: "owned-only" });
154
+ expect(seen).toMatchObject({ manifest: "k8s.yaml", documents: [deployment()], environment: "prod", stack: "web", deleteMode: "owned-only" });
155
+ expect(planned.member).toBe("web");
156
+ expect(isPlanDigest(planned.planDigest)).toBe(true);
157
+ expect(planned.changeSet!.member).toMatchObject({ member: "web", lexicon: "k8s", planner: "chant", status: "planned", planDigest: planned.planDigest, holes: [] });
158
+ expect(planned.changeSet!.entries).toEqual([
159
+ {
160
+ member: "web",
161
+ lexicon: "k8s",
162
+ planner: "chant",
163
+ address: "Deployment/prod/web",
164
+ type: "apps/v1/Deployment",
165
+ name: "web",
166
+ action: "update",
167
+ disruption: "in-place",
168
+ attributes: [{ path: "spec.replicas", before: 3, after: 5 }],
169
+ },
170
+ ]);
171
+ });
172
+
173
+ test("without a stack the component is the member", async () => {
174
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async () => unchanged());
175
+ expect((await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml" })).member).toBe("api");
176
+ });
177
+
178
+ test("a manifest equal to the live objects plans as no change", async () => {
179
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async () => unchanged());
180
+ const planned = await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml", stack: "web" });
181
+ expect(planned.changeSet!.entries.map((e) => e.action)).toEqual(["no-op"]);
182
+ });
183
+
184
+ test("creates list the declared fields, a Secret's data marked sensitive, deletes are destroys, a pod-template change rolls", async () => {
185
+ const dryRun: DryRun = {
186
+ fieldManager: "chant:web",
187
+ objects: [
188
+ {
189
+ ref: { apiVersion: "v1", kind: "Secret", name: "s", namespace: "prod" },
190
+ action: "create",
191
+ object: { apiVersion: "v1", kind: "Secret", metadata: { name: "s", namespace: "prod" }, data: { password: "aHVudGVyMg==" } },
192
+ changes: [],
193
+ },
194
+ { ref: web, action: "update", changes: [{ path: "spec.template.spec.containers", before: [{ image: "a" }], after: [{ image: "b" }] }] },
195
+ ],
196
+ deletes: [{ apiVersion: "v1", kind: "ConfigMap", name: "old", namespace: "prod" }],
197
+ retained: [],
198
+ };
199
+ const cap = createKubectlApplyCapability(noApply, () => [], async () => dryRun);
200
+ const { entries } = (await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml", stack: "web", delete: "owned-only" })).changeSet!;
201
+ expect(entries[0]).toMatchObject({
202
+ action: "create",
203
+ address: "Secret/prod/s",
204
+ attributes: [{ path: "data.password", sensitive: true }, { path: "metadata.name", after: "s" }, { path: "metadata.namespace", after: "prod" }],
205
+ });
206
+ expect(JSON.stringify(entries)).not.toContain("aHVudGVyMg==");
207
+ expect(entries[1]).toMatchObject({ action: "update", disruption: "rolling" });
208
+ expect(entries[2]).toMatchObject({ action: "delete", disruption: "destroy", address: "ConfigMap/prod/old", type: "v1/ConfigMap" });
209
+ });
210
+
211
+ test("an immutable-field refusal is an update with unknown disruption and a hole saying the apply would fail", async () => {
212
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async () => ({
213
+ fieldManager: "chant:web",
214
+ objects: [{ ref: web, action: "update", changes: [], refused: "spec.selector: field is immutable" }],
215
+ deletes: [],
216
+ retained: [],
217
+ }));
218
+ const part = (await cap.plan!({ env: "prod", component: "api" }, { manifest: "k8s.yaml", stack: "web" })).changeSet!;
219
+ expect(part.entries[0]).toMatchObject({ action: "update", disruption: "unknown", attributes: [] });
220
+ expect(part.member.holes).toEqual([
221
+ expect.objectContaining({ address: "Deployment/prod/web", reason: expect.stringContaining("field is immutable") }),
222
+ ]);
223
+ });
224
+
225
+ test("the digest is stable for the same dry run and moves with what it would change", async () => {
226
+ let to = 5;
227
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async () => scaled(to));
228
+ const ctx = { env: "prod", component: "api" };
229
+ const input = { manifest: "k8s.yaml", stack: "web" };
230
+ const a = (await cap.plan!(ctx, input)).planDigest;
231
+ expect((await cap.plan!(ctx, input)).planDigest).toBe(a);
232
+ to = 6;
233
+ expect((await cap.plan!(ctx, input)).planDigest).not.toBe(a);
234
+ to = 5;
235
+ expect((await cap.plan!({ ...ctx, env: "staging" }, input)).planDigest).not.toBe(a);
236
+ });
237
+
238
+ test("a live value moving under a field the apply sets to the same value leaves the digest alone", async () => {
239
+ let from = 3;
240
+ const cap = createKubectlApplyCapability(noApply, () => [deployment()], async () => ({
241
+ fieldManager: "chant:web",
242
+ objects: [{ ref: web, action: "update", changes: [{ path: "spec.replicas", before: from, after: 5 }] }],
243
+ deletes: [],
244
+ retained: [],
245
+ }));
246
+ const ctx = { env: "prod", component: "api" };
247
+ const a = (await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web" })).planDigest;
248
+ from = 4;
249
+ expect((await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web" })).planDigest).toBe(a);
250
+ });
251
+
252
+ test("release attributes are set before the dry run, so the plan covers what run applies", async () => {
253
+ const stampedDeployment = () => ({
254
+ apiVersion: "apps/v1",
255
+ kind: "Deployment",
256
+ metadata: { name: "api" },
257
+ spec: {
258
+ template: {
259
+ metadata: {},
260
+ spec: { containers: [{ name: "api", image: "x", env: [{ name: "CHANT_RELEASE_ATTRIBUTES", valueFrom: { fieldRef: { fieldPath: "metadata.annotations['chant.intentius.io/release-attributes']" } } }] }] },
261
+ },
262
+ },
263
+ });
264
+ let planned: Record<string, any>[] = [];
265
+ const cap = createKubectlApplyCapability(noApply, () => [stampedDeployment()], async (args) => {
266
+ planned = args.documents as never;
267
+ return unchanged();
268
+ });
269
+ const result = await cap.plan!({ env: "prod", component: "api", release: { revision: "0123abc" } }, { manifest: "k8s.yaml", stack: "web" });
270
+ expect(planned[0].spec.template.metadata.annotations["chant.intentius.io/release-attributes"]).toContain("0123abc");
271
+ expect((result.artifact as { documents: unknown[] }).documents).toEqual(planned);
272
+ });
273
+
274
+ test("run handed the plan applies the planned documents after a fresh dry run agrees", async () => {
275
+ let to = 5;
276
+ const applied: Record<string, unknown>[] = [];
277
+ let reads = 0;
278
+ const cap = createKubectlApplyCapability(
279
+ async (args) => {
280
+ applied.push(args as never);
281
+ return { applied: [web], pruned: [], fieldManager: "chant:web" };
282
+ },
283
+ () => {
284
+ reads++;
285
+ return [deployment()];
286
+ },
287
+ async () => scaled(to),
288
+ );
289
+ const input = { manifest: "k8s.yaml", stack: "web", delete: "owned-only" as const };
290
+ const planned = await cap.plan!({ env: "prod", component: "api" }, input);
291
+ await cap.run({ env: "prod", component: "api", plans: { web: planned.artifact } }, input);
292
+ expect(reads).toBe(1);
293
+ expect(applied).toEqual([{ manifest: "k8s.yaml", documents: [deployment()], environment: "prod", stack: "web", deleteMode: "owned-only" }]);
294
+
295
+ to = 7;
296
+ const { KubectlApplyPlanMovedError } = await import("./kubectl-apply");
297
+ const moved = cap.run({ env: "prod", component: "api", plans: { web: planned.artifact } }, input);
298
+ await expect(moved).rejects.toBeInstanceOf(KubectlApplyPlanMovedError);
299
+ await expect(moved).rejects.toMatchObject({ planMoved: true, exitCode: 3, approved: planned.planDigest });
300
+ // Core's mark for a moved plan, so the command that applied it exits 3 (#3997).
301
+ const { isPlanMovedError } = await import("@intentius/chant/components/capability");
302
+ expect(isPlanMovedError(await moved.catch((e: unknown) => e))).toBe(true);
303
+ expect(applied).toHaveLength(1);
304
+ });
305
+
306
+ test("a plan for another member is not this step's: run applies as it would without one", async () => {
307
+ const applied: Record<string, unknown>[] = [];
308
+ const cap = createKubectlApplyCapability(
309
+ async (args) => {
310
+ applied.push(args as never);
311
+ return { applied: [], pruned: [], fieldManager: "chant:web" };
312
+ },
313
+ () => [deployment()],
314
+ async () => {
315
+ throw new Error("no dry run expected");
316
+ },
317
+ );
318
+ await cap.run({ env: "prod", component: "api", plans: { other: { kind: "kubectl-apply-plan", planDigest: "sha256:x", documents: [] } } }, { manifest: "k8s.yaml", stack: "web" });
319
+ expect(applied).toEqual([{ manifest: "k8s.yaml", environment: "prod", stack: "web" }]);
320
+ });
321
+ });
322
+
323
+ describe("kubectl-apply outputs: the fields a step names (#3999)", () => {
324
+ type DryRun = import("./kubectl-apply").KubectlApplyDryRun;
325
+ const svcRef = { apiVersion: "v1", kind: "Service", name: "web", namespace: "prod" };
326
+ const service = () => ({ apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod" }, spec: { type: "LoadBalancer", ports: [{ port: 80 }] } });
327
+ const live = () => ({ ...service(), spec: { ...service().spec, clusterIP: "10.0.0.9" }, status: { loadBalancer: { ingress: [{ hostname: "web.example" }] } } });
328
+ const outputs = {
329
+ host: { object: "Service/prod/web", field: "status.loadBalancer.ingress.0.hostname" },
330
+ port: { object: "Service/prod/web", field: "spec.ports.0.port" },
331
+ };
332
+ const dryRun = (action: "create" | "update" | "no-op"): DryRun => ({ fieldManager: "chant:web", stack: "web", objects: [{ ref: svcRef, action, changes: [] }], deletes: [], retained: [] });
333
+ const ctx = { env: "prod", component: "web" };
334
+ const applied = async () => ({ applied: [], pruned: [], fieldManager: "chant:web" });
335
+
336
+ test("outputs reads each named field from the cluster, applying nothing; a step that names none has none", async () => {
337
+ const reads: unknown[] = [];
338
+ const cap = createKubectlApplyCapability(
339
+ async () => {
340
+ throw new Error("applied");
341
+ },
342
+ () => [service()],
343
+ undefined,
344
+ async (args) => {
345
+ reads.push(args);
346
+ return [live()];
347
+ },
348
+ );
349
+ expect(await cap.outputs!(ctx, { manifest: "k8s.yaml", stack: "web", outputs })).toEqual({ host: "web.example", port: 80 });
350
+ expect(reads).toEqual([{ refs: [svcRef], environment: "prod" }]);
351
+ expect(await cap.outputs!(ctx, { manifest: "k8s.yaml", stack: "web" })).toEqual({});
352
+ });
353
+
354
+ test("an object not made yet leaves its outputs out", async () => {
355
+ const cap = createKubectlApplyCapability(applied, () => [service()], undefined, async () => [undefined]);
356
+ expect(await cap.outputs!(ctx, { manifest: "k8s.yaml", stack: "web", outputs })).toEqual({});
357
+ });
358
+
359
+ test("run returns the named fields as outputs, read once the apply landed, which a dependent's stackOutput() reads", async () => {
360
+ const cap = createKubectlApplyCapability(applied, () => [service()], undefined, async () => [live()]);
361
+ const out = await cap.run(ctx, { manifest: "k8s.yaml", stack: "web", outputs });
362
+ expect(out.outputs).toEqual({ host: "web.example", port: 80 });
363
+ });
364
+
365
+ test("an output naming an object the manifest does not declare is an error that says what it declares", async () => {
366
+ const cap = createKubectlApplyCapability(applied, () => [service()], undefined, async () => [live()]);
367
+ await expect(cap.outputs!(ctx, { manifest: "k8s.yaml", stack: "web", outputs: { x: { object: "Service/prod/api", field: "spec" } } })).rejects.toThrow(
368
+ 'kubectl-apply "web": output "x" names Service/prod/api, which k8s.yaml does not declare (it declares Service/prod/web)',
369
+ );
370
+ });
371
+
372
+ test("a plan knows a field the manifest sets; a field the server fills is known only after apply", async () => {
373
+ const cap = createKubectlApplyCapability(applied, () => [service()], async () => dryRun("create"), async () => {
374
+ throw new Error("a created object has nothing live to read");
375
+ });
376
+ const planned = await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web", outputs });
377
+ expect(planned.outputs).toEqual({ known: { port: 80 }, unknown: ["host"] });
378
+ });
379
+
380
+ test("a plan that leaves the object as it is knows every named field, as the cluster holds it", async () => {
381
+ const cap = createKubectlApplyCapability(applied, () => [service()], async () => dryRun("no-op"), async () => [live()]);
382
+ const planned = await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web", outputs });
383
+ expect(planned.outputs).toEqual({ known: { host: "web.example", port: 80 }, unknown: [] });
384
+ });
385
+
386
+ test("a step that names no outputs plans none", async () => {
387
+ const cap = createKubectlApplyCapability(applied, () => [service()], async () => dryRun("update"));
388
+ expect((await cap.plan!(ctx, { manifest: "k8s.yaml", stack: "web" })).outputs).toBeUndefined();
389
+ });
390
+ });