@intentius/chant-lexicon-k8s 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/composites/index.d.ts +2 -0
  2. package/dist/composites/index.d.ts.map +1 -1
  3. package/dist/composites/operator-stack.d.ts +183 -0
  4. package/dist/composites/operator-stack.d.ts.map +1 -0
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/integrity.json +35 -35
  8. package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
  9. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  10. package/dist/lint/post-synth/k8s-helpers.d.ts +28 -5
  11. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  12. package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
  13. package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
  14. package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
  15. package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
  16. package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
  17. package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
  18. package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
  19. package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
  20. package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
  21. package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
  22. package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
  23. package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
  24. package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
  25. package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
  26. package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
  27. package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
  28. package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
  29. package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
  30. package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
  31. package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
  32. package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
  33. package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
  34. package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
  35. package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
  36. package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
  37. package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
  38. package/dist/lint/post-synth/wk8404.d.ts.map +1 -1
  39. package/dist/lint/post-synth/wk8405.d.ts.map +1 -1
  40. package/dist/lint/post-synth/wk8406.d.ts.map +1 -1
  41. package/dist/lint/post-synth/wk8407.d.ts.map +1 -1
  42. package/dist/manifest.json +1 -1
  43. package/dist/op/builders.d.ts +69 -0
  44. package/dist/op/builders.d.ts.map +1 -0
  45. package/dist/rules/argo-helpers.ts +2 -6
  46. package/dist/rules/crd-schema-helpers.ts +4 -6
  47. package/dist/rules/k8s-helpers.ts +32 -23
  48. package/dist/rules/wk8005.ts +27 -32
  49. package/dist/rules/wk8006.ts +33 -38
  50. package/dist/rules/wk8041.ts +19 -24
  51. package/dist/rules/wk8042.ts +17 -22
  52. package/dist/rules/wk8101.ts +29 -34
  53. package/dist/rules/wk8102.ts +13 -18
  54. package/dist/rules/wk8103.ts +17 -22
  55. package/dist/rules/wk8104.ts +30 -35
  56. package/dist/rules/wk8105.ts +16 -21
  57. package/dist/rules/wk8201.ts +26 -31
  58. package/dist/rules/wk8202.ts +17 -22
  59. package/dist/rules/wk8203.ts +17 -22
  60. package/dist/rules/wk8204.ts +46 -51
  61. package/dist/rules/wk8205.ts +27 -32
  62. package/dist/rules/wk8207.ts +14 -19
  63. package/dist/rules/wk8208.ts +14 -19
  64. package/dist/rules/wk8209.ts +14 -19
  65. package/dist/rules/wk8301.ts +25 -30
  66. package/dist/rules/wk8302.ts +19 -24
  67. package/dist/rules/wk8303.ts +23 -26
  68. package/dist/rules/wk8304.ts +35 -40
  69. package/dist/rules/wk8305.ts +39 -42
  70. package/dist/rules/wk8306.ts +20 -25
  71. package/dist/rules/wk8401.ts +59 -64
  72. package/dist/rules/wk8402.ts +13 -18
  73. package/dist/rules/wk8403.ts +32 -37
  74. package/dist/rules/wk8404.ts +15 -21
  75. package/dist/rules/wk8405.ts +15 -18
  76. package/dist/rules/wk8406.ts +29 -34
  77. package/dist/rules/wk8407.ts +17 -22
  78. package/dist/serializer.d.ts.map +1 -1
  79. package/package.json +3 -3
  80. package/src/composites/composites.test.ts +283 -0
  81. package/src/composites/index.ts +4 -0
  82. package/src/composites/operator-stack.ts +385 -0
  83. package/src/index.ts +11 -0
  84. package/src/lint/post-synth/argo-helpers.ts +2 -6
  85. package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
  86. package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
  87. package/src/lint/post-synth/k8s-helpers.ts +32 -23
  88. package/src/lint/post-synth/wk8005.ts +27 -32
  89. package/src/lint/post-synth/wk8006.ts +33 -38
  90. package/src/lint/post-synth/wk8041.ts +19 -24
  91. package/src/lint/post-synth/wk8042.ts +17 -22
  92. package/src/lint/post-synth/wk8101.ts +29 -34
  93. package/src/lint/post-synth/wk8102.ts +13 -18
  94. package/src/lint/post-synth/wk8103.ts +17 -22
  95. package/src/lint/post-synth/wk8104.ts +30 -35
  96. package/src/lint/post-synth/wk8105.ts +16 -21
  97. package/src/lint/post-synth/wk8201.ts +26 -31
  98. package/src/lint/post-synth/wk8202.ts +17 -22
  99. package/src/lint/post-synth/wk8203.ts +17 -22
  100. package/src/lint/post-synth/wk8204.ts +46 -51
  101. package/src/lint/post-synth/wk8205.ts +27 -32
  102. package/src/lint/post-synth/wk8207.ts +14 -19
  103. package/src/lint/post-synth/wk8208.ts +14 -19
  104. package/src/lint/post-synth/wk8209.ts +14 -19
  105. package/src/lint/post-synth/wk8301.ts +25 -30
  106. package/src/lint/post-synth/wk8302.ts +19 -24
  107. package/src/lint/post-synth/wk8303.ts +23 -26
  108. package/src/lint/post-synth/wk8304.ts +35 -40
  109. package/src/lint/post-synth/wk8305.ts +39 -42
  110. package/src/lint/post-synth/wk8306.ts +20 -25
  111. package/src/lint/post-synth/wk8401.ts +59 -64
  112. package/src/lint/post-synth/wk8402.ts +13 -18
  113. package/src/lint/post-synth/wk8403.ts +32 -37
  114. package/src/lint/post-synth/wk8404.ts +15 -21
  115. package/src/lint/post-synth/wk8405.ts +15 -18
  116. package/src/lint/post-synth/wk8406.ts +29 -34
  117. package/src/lint/post-synth/wk8407.ts +17 -22
  118. package/src/op/builders.test.ts +82 -0
  119. package/src/op/builders.ts +95 -0
  120. package/src/serializer.ts +9 -1
@@ -1 +1 @@
1
- {"version":3,"file":"wk8404.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8404.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAW9G,eAAO,MAAM,MAAM,EAAE,cAiCpB,CAAC"}
1
+ {"version":3,"file":"wk8404.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8404.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAU9G,eAAO,MAAM,MAAM,EAAE,cA4BpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"wk8405.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8405.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAc9G,eAAO,MAAM,MAAM,EAAE,cAiCpB,CAAC"}
1
+ {"version":3,"file":"wk8405.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8405.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAc9G,eAAO,MAAM,MAAM,EAAE,cA8BpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"wk8406.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8406.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAG9G,eAAO,MAAM,MAAM,EAAE,cA8CpB,CAAC"}
1
+ {"version":3,"file":"wk8406.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8406.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAG9G,eAAO,MAAM,MAAM,EAAE,cAyCpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"wk8407.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8407.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAM9G;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAS9D;AAED,eAAO,MAAM,MAAM,EAAE,cAmCpB,CAAC"}
1
+ {"version":3,"file":"wk8407.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/wk8407.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAM9G;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAS9D;AAED,eAAO,MAAM,MAAM,EAAE,cA8BpB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k8s",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "K8s",
6
6
  "intrinsics": [],
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Typed step-builder wrappers for this lexicon's activities (chant #1288
3
+ * Stage 2 — "regenerate the step builders as fully typed wrappers"). Core
4
+ * cannot import a lexicon's `*Args` types (lexicons depend on core, not the
5
+ * other way around — see `packages/core/src/op/activity-contract.ts`'s module
6
+ * doc for the same layering constraint on Stage 1's contracts), so a fully
7
+ * typed `kubectlApply`/`waitForReady`/`ensureSecret` has to live beside the
8
+ * `*Args` interface it derives from: here, next to `./activities/kubectl.ts`
9
+ * etc. `opts`'s type in each wrapper below IS the activity's own `*Args`
10
+ * interface (via `Omit`/`WithStepRefs`) — never a hand-restated mirror — so
11
+ * `kubectlApply`/`waitForReady`/`ensureSecret`'s full option surface (the
12
+ * dozen-plus cross-field options on `kubectlApply` that Stage 1's zod
13
+ * contracts deliberately skipped rather than duplicate) is authoring-time
14
+ * typed for free, and stays in sync with the implementation by construction.
15
+ *
16
+ * Every field also accepts a {@link StepOutputRef} in its place
17
+ * ({@link WithStepRefs}, chant #1950) — a later step's args can consume an
18
+ * earlier step's declared output.
19
+ *
20
+ * `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
21
+ * `@intentius/chant/op`, re-exported from `@intentius/chant-lexicon-temporal`
22
+ * for lexicon-agnostic single-import convenience) are UNCHANGED and produce
23
+ * byte-identical `ActivityStep` output for the same inputs — these are purely
24
+ * additive. Deliberately not swapped into the temporal barrel: that would
25
+ * make `@intentius/chant-lexicon-temporal` depend on this package (and on
26
+ * helm, for `helmInstall`) at runtime, which is exactly the "a worker image
27
+ * needs no kubectl binary" / "temporal stays product-agnostic" property the
28
+ * `op/activities/index.ts` module docs across this repo call out on purpose
29
+ * (activities were moved OUT of temporal into per-product lexicons for this
30
+ * reason; the step-builder layer shouldn't reintroduce the coupling from the
31
+ * other direction). An author who wants the typed surface imports it from
32
+ * here — `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
33
+ * already depends on; existing `@intentius/chant-lexicon-temporal` imports
34
+ * keep working exactly as before, opting in only if the import is changed.
35
+ */
36
+ import { type ActivityStep, type NamedActivityStep, type WithStepRefs } from "@intentius/chant/op";
37
+ import type { KubectlApplyArgs } from "./activities/kubectl.js";
38
+ import type { WaitForReadyArgs } from "./activities/wait-for-ready.js";
39
+ import type { EnsureSecretActivityArgs } from "./activities/ensure-secret.js";
40
+ /** Extra opts every wrapper below accepts alongside its activity's own fields. */
41
+ type StepOpts = {
42
+ profile?: ActivityStep["profile"];
43
+ id?: string;
44
+ };
45
+ /**
46
+ * Run `kubectl apply -f <manifest>` — the fully typed twin of core's
47
+ * `kubectlApply` step-builder shortcut (chant #1288 Stage 2). `opts` is
48
+ * {@link KubectlApplyArgs} itself (minus the positional `manifest`) — every
49
+ * field `applyManifest` accepts, including `deleteMode`, `force`,
50
+ * `fieldManager`, `documents`, is authoring-time typed here, not just the
51
+ * subset a hand-written zod mirror would bother restating. Defaults to the
52
+ * `longInfra` profile (override via `opts.profile`).
53
+ */
54
+ export declare const kubectlApply: (manifest: string, opts?: WithStepRefs<Omit<KubectlApplyArgs, "manifest">> & StepOpts) => NamedActivityStep;
55
+ /**
56
+ * Poll a Kubernetes resource until it reports ready, driven by a data-only
57
+ * readiness spec (CRD-aware; #365) — the fully typed twin of core's
58
+ * `waitForReady`. `opts` is {@link WaitForReadyArgs} itself, minus the
59
+ * positional `kind`/`name`. Defaults to the `k8sWait` profile.
60
+ */
61
+ export declare const waitForReady: (kind: string, name: string, opts?: WithStepRefs<Omit<WaitForReadyArgs, "kind" | "name">> & StepOpts) => NamedActivityStep;
62
+ /**
63
+ * Ensure a `generated-once` secret exists in the target store (#1829) — the
64
+ * fully typed twin of core's `ensureSecret`. `opts` is
65
+ * {@link EnsureSecretActivityArgs} itself, minus the positional `name`/`keys`.
66
+ */
67
+ export declare const ensureSecret: (name: string, keys: string[], opts?: WithStepRefs<Omit<EnsureSecretActivityArgs, "name" | "keys">> & StepOpts) => NamedActivityStep;
68
+ export {};
69
+ //# sourceMappingURL=builders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/op/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,kFAAkF;AAClF,KAAK,QAAQ,GAAG;IAAE,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GACvB,UAAU,MAAM,EAChB,OAAO,YAAY,CAAC,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC,GAAG,QAAQ,KACjE,iBAGF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAO,YAAY,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,QAAQ,KACtE,iBAGF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACvB,MAAM,MAAM,EACZ,MAAM,MAAM,EAAE,EACd,OAAO,YAAY,CAAC,IAAI,CAAC,wBAAwB,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,QAAQ,KAC9E,iBAGF,CAAC"}
@@ -6,7 +6,7 @@
6
6
  * Excluded from check auto-discovery by the "helper" filename filter.
7
7
  */
8
8
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
9
- import { getPrimaryOutput, parseK8sManifests, type K8sManifest } from "./k8s-helpers";
9
+ import { docsToManifests, type K8sManifest } from "./k8s-helpers";
10
10
 
11
11
  /** The always-present, in-cluster Argo destination. */
12
12
  export const IN_CLUSTER_SERVER = "https://kubernetes.default.svc";
@@ -17,11 +17,7 @@ export const CLUSTER_SECRET_TYPE_LABEL = "argocd.argoproj.io/secret-type";
17
17
 
18
18
  /** All manifests across every lexicon output. */
19
19
  export function allManifests(ctx: PostSynthContext): K8sManifest[] {
20
- const manifests: K8sManifest[] = [];
21
- for (const [, output] of ctx.outputs) {
22
- manifests.push(...parseK8sManifests(getPrimaryOutput(output)));
23
- }
24
- return manifests;
20
+ return docsToManifests(ctx);
25
21
  }
26
22
 
27
23
  /** Manifests of a given Argo kind. */
@@ -14,7 +14,7 @@
14
14
  import { createRequire } from "module";
15
15
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
16
16
  import type { CrdFieldSchema } from "../../spec/parse";
17
- import { getPrimaryOutput, parseK8sManifests, type K8sManifest } from "./k8s-helpers";
17
+ import { docsToManifests, type K8sManifest } from "./k8s-helpers";
18
18
 
19
19
  export type { CrdFieldSchema };
20
20
 
@@ -70,11 +70,9 @@ export function specSchemaFor(manifest: K8sManifest): CrdFieldSchema | undefined
70
70
  /** Every manifest in the build that has a shipped spec schema, with that schema. */
71
71
  export function customResources(ctx: PostSynthContext): Array<{ manifest: K8sManifest; schema: CrdFieldSchema }> {
72
72
  const out: Array<{ manifest: K8sManifest; schema: CrdFieldSchema }> = [];
73
- for (const [, output] of ctx.outputs) {
74
- for (const manifest of parseK8sManifests(getPrimaryOutput(output))) {
75
- const schema = specSchemaFor(manifest);
76
- if (schema) out.push({ manifest, schema });
77
- }
73
+ for (const manifest of docsToManifests(ctx)) {
74
+ const schema = specSchemaFor(manifest);
75
+ if (schema) out.push({ manifest, schema });
78
76
  }
79
77
  return out;
80
78
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Shared helpers for Kubernetes post-synthesis lint rules.
3
3
  *
4
- * Provides YAML parsing for multi-document K8s manifests and container
5
- * extraction logic that handles all common workload types.
4
+ * Provides the `ctx.docs` `K8sManifest[]` bridge every check reads through,
5
+ * and container extraction logic that handles all common workload types.
6
6
  */
7
7
 
8
- import { parseYAML } from "@intentius/chant/yaml";
8
+ import { parseOutputDocs, type PostSynthContext } from "@intentius/chant/lint/post-synth";
9
9
  export { getPrimaryOutput, getAdditionalFiles } from "@intentius/chant/lint/post-synth";
10
10
 
11
11
  /**
@@ -45,27 +45,36 @@ export interface K8sContainer {
45
45
  }
46
46
 
47
47
  /**
48
- * Split a multi-document YAML string on `---` boundaries and parse each
49
- * document into a K8sManifest.
48
+ * Every parsed Kubernetes manifest across all lexicon outputs, via `ctx.docs`
49
+ * (chant #975) the shared, parse-once view every k8s post-synth check
50
+ * reads, in place of the old per-check `parseK8sManifests(getPrimaryOutput(output))`
51
+ * splitter (chant #976). Two documents are dropped rather than passed through
52
+ * as a "manifest":
53
+ *
54
+ * - `d.error` — a document `parseOutputDocs` could not make sense of.
55
+ * - `d.file` — a document that came from a `SerializerResult.files` sidecar
56
+ * (a committed-encrypted secret's ciphertext, in this lexicon) rather than
57
+ * a lexicon's primary output. `parseK8sManifests` only ever saw
58
+ * `getPrimaryOutput(output)`, so a sidecar was invisible to every general
59
+ * manifest-consuming check — see `getAdditionalFiles`'s doc comment on why
60
+ * that separation is deliberate. Only `sops-helpers.ts`'s
61
+ * `resolveEncryptedSecretClaims` (WK8504/WK8503's producer join) reads
62
+ * `.files` explicitly; this bridge must keep it that way.
63
+ *
64
+ * `ctx.docs` is populated by every context chant itself builds (`chant
65
+ * build`, `@intentius/chant-test-utils`'s `createPostSynthContext` /
66
+ * `makePostSynthCtx*`) — see `PostSynthContext.docs`'s doc comment for why
67
+ * it is still optional at the type level. A handful of this lexicon's own
68
+ * hand-rolled `PostSynthContext` test literals (`post-synth.test.ts`'s
69
+ * `makeCtx`) predate that field and never set it; for those, fall back to
70
+ * parsing `ctx.outputs` directly with the same `parseOutputDocs` primitive,
71
+ * rather than silently returning no manifests.
50
72
  */
51
- export function parseK8sManifests(yaml: string): K8sManifest[] {
52
- const documents = yaml.split(/\n---\n/);
53
- const manifests: K8sManifest[] = [];
54
-
55
- for (const doc of documents) {
56
- const trimmed = doc.trim();
57
- if (trimmed === "" || trimmed === "---") continue;
58
- try {
59
- const parsed = parseYAML(trimmed);
60
- if (typeof parsed === "object" && parsed !== null) {
61
- manifests.push(parsed as K8sManifest);
62
- }
63
- } catch {
64
- // Skip unparseable documents
65
- }
66
- }
67
-
68
- return manifests;
73
+ export function docsToManifests(ctx: PostSynthContext): K8sManifest[] {
74
+ const docs = ctx.docs ?? parseOutputDocs(ctx.outputs);
75
+ return docs
76
+ .filter((d) => !d.error && !d.file)
77
+ .map((d) => d.value as K8sManifest);
69
78
  }
70
79
 
71
80
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
- import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
10
+ import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
11
11
 
12
12
  const SENSITIVE_NAME_PATTERN = /password|token|key|secret/i;
13
13
 
@@ -18,37 +18,32 @@ export const wk8005: PostSynthCheck = {
18
18
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
19
  const diagnostics: PostSynthDiagnostic[] = [];
20
20
 
21
- for (const [, output] of ctx.outputs) {
22
- const yaml = getPrimaryOutput(output);
23
- const manifests = parseK8sManifests(yaml);
24
-
25
- for (const manifest of manifests) {
26
- if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
27
-
28
- const containers = extractContainers(manifest);
29
- const resourceName = manifest.metadata?.name ?? manifest.kind;
30
-
31
- for (const container of containers) {
32
- if (!Array.isArray(container.env)) continue;
33
-
34
- for (const envVar of container.env) {
35
- if (
36
- typeof envVar.name === "string" &&
37
- SENSITIVE_NAME_PATTERN.test(envVar.name) &&
38
- envVar.value !== undefined &&
39
- envVar.value !== null &&
40
- typeof envVar.value === "string" &&
41
- envVar.value !== "" &&
42
- !envVar.valueFrom
43
- ) {
44
- diagnostics.push({
45
- checkId: "WK8005",
46
- severity: "error",
47
- message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has hardcoded value for sensitive env var "${envVar.name}" — use secretKeyRef instead`,
48
- entity: resourceName,
49
- lexicon: "k8s",
50
- });
51
- }
21
+ for (const manifest of docsToManifests(ctx)) {
22
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
23
+
24
+ const containers = extractContainers(manifest);
25
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
26
+
27
+ for (const container of containers) {
28
+ if (!Array.isArray(container.env)) continue;
29
+
30
+ for (const envVar of container.env) {
31
+ if (
32
+ typeof envVar.name === "string" &&
33
+ SENSITIVE_NAME_PATTERN.test(envVar.name) &&
34
+ envVar.value !== undefined &&
35
+ envVar.value !== null &&
36
+ typeof envVar.value === "string" &&
37
+ envVar.value !== "" &&
38
+ !envVar.valueFrom
39
+ ) {
40
+ diagnostics.push({
41
+ checkId: "WK8005",
42
+ severity: "error",
43
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has hardcoded value for sensitive env var "${envVar.name}" — use secretKeyRef instead`,
44
+ entity: resourceName,
45
+ lexicon: "k8s",
46
+ });
52
47
  }
53
48
  }
54
49
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
- import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
9
+ import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
10
10
 
11
11
  export const wk8006: PostSynthCheck = {
12
12
  id: "WK8006",
@@ -15,49 +15,44 @@ export const wk8006: PostSynthCheck = {
15
15
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
16
  const diagnostics: PostSynthDiagnostic[] = [];
17
17
 
18
- for (const [, output] of ctx.outputs) {
19
- const yaml = getPrimaryOutput(output);
20
- const manifests = parseK8sManifests(yaml);
21
-
22
- for (const manifest of manifests) {
23
- if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
24
-
25
- const containers = extractContainers(manifest);
26
- const resourceName = manifest.metadata?.name ?? manifest.kind;
27
-
28
- for (const container of containers) {
29
- const image = container.image;
30
- if (typeof image !== "string" || image === "") continue;
31
-
32
- // Split off digest (@sha256:...) those are pinned and fine
33
- if (image.includes("@")) continue;
34
-
35
- // Extract the tag portion (after the last colon that isn't part of a port)
36
- // Images can be: name, name:tag, registry:port/name, registry:port/name:tag
37
- const slashIndex = image.lastIndexOf("/");
38
- const afterSlash = slashIndex >= 0 ? image.slice(slashIndex + 1) : image;
39
- const colonIndex = afterSlash.lastIndexOf(":");
40
-
41
- if (colonIndex === -1) {
42
- // No tag at all
18
+ for (const manifest of docsToManifests(ctx)) {
19
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
20
+
21
+ const containers = extractContainers(manifest);
22
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
23
+
24
+ for (const container of containers) {
25
+ const image = container.image;
26
+ if (typeof image !== "string" || image === "") continue;
27
+
28
+ // Split off digest (@sha256:...) — those are pinned and fine
29
+ if (image.includes("@")) continue;
30
+
31
+ // Extract the tag portion (after the last colon that isn't part of a port)
32
+ // Images can be: name, name:tag, registry:port/name, registry:port/name:tag
33
+ const slashIndex = image.lastIndexOf("/");
34
+ const afterSlash = slashIndex >= 0 ? image.slice(slashIndex + 1) : image;
35
+ const colonIndex = afterSlash.lastIndexOf(":");
36
+
37
+ if (colonIndex === -1) {
38
+ // No tag at all
39
+ diagnostics.push({
40
+ checkId: "WK8006",
41
+ severity: "warning",
42
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses untagged image "${image}" — specify an explicit version tag`,
43
+ entity: resourceName,
44
+ lexicon: "k8s",
45
+ });
46
+ } else {
47
+ const tag = afterSlash.slice(colonIndex + 1);
48
+ if (tag === "latest") {
43
49
  diagnostics.push({
44
50
  checkId: "WK8006",
45
51
  severity: "warning",
46
- message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses untagged image "${image}" — specify an explicit version tag`,
52
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses :latest tag on image "${image}" — use a specific version tag`,
47
53
  entity: resourceName,
48
54
  lexicon: "k8s",
49
55
  });
50
- } else {
51
- const tag = afterSlash.slice(colonIndex + 1);
52
- if (tag === "latest") {
53
- diagnostics.push({
54
- checkId: "WK8006",
55
- severity: "warning",
56
- message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses :latest tag on image "${image}" — use a specific version tag`,
57
- entity: resourceName,
58
- lexicon: "k8s",
59
- });
60
- }
61
56
  }
62
57
  }
63
58
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
- import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
10
+ import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
11
11
 
12
12
  /**
13
13
  * Known API key patterns and their descriptions.
@@ -35,33 +35,28 @@ export const wk8041: PostSynthCheck = {
35
35
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
36
36
  const diagnostics: PostSynthDiagnostic[] = [];
37
37
 
38
- for (const [, output] of ctx.outputs) {
39
- const yaml = getPrimaryOutput(output);
40
- const manifests = parseK8sManifests(yaml);
38
+ for (const manifest of docsToManifests(ctx)) {
39
+ if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
41
40
 
42
- for (const manifest of manifests) {
43
- if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
41
+ const containers = extractContainers(manifest);
42
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
44
43
 
45
- const containers = extractContainers(manifest);
46
- const resourceName = manifest.metadata?.name ?? manifest.kind;
44
+ for (const container of containers) {
45
+ if (!Array.isArray(container.env)) continue;
47
46
 
48
- for (const container of containers) {
49
- if (!Array.isArray(container.env)) continue;
47
+ for (const envVar of container.env) {
48
+ if (typeof envVar.value !== "string" || envVar.value === "") continue;
50
49
 
51
- for (const envVar of container.env) {
52
- if (typeof envVar.value !== "string" || envVar.value === "") continue;
53
-
54
- for (const { pattern, label } of API_KEY_PATTERNS) {
55
- if (pattern.test(envVar.value)) {
56
- diagnostics.push({
57
- checkId: "WK8041",
58
- severity: "error",
59
- message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has a ${label} hardcoded in env var "${envVar.name ?? "(unnamed)"}" — use a Secret reference instead`,
60
- entity: resourceName,
61
- lexicon: "k8s",
62
- });
63
- break; // One match per env var is sufficient
64
- }
50
+ for (const { pattern, label } of API_KEY_PATTERNS) {
51
+ if (pattern.test(envVar.value)) {
52
+ diagnostics.push({
53
+ checkId: "WK8041",
54
+ severity: "error",
55
+ message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has a ${label} hardcoded in env var "${envVar.name ?? "(unnamed)"}" — use a Secret reference instead`,
56
+ entity: resourceName,
57
+ lexicon: "k8s",
58
+ });
59
+ break; // One match per env var is sufficient
65
60
  }
66
61
  }
67
62
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
- import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
10
+ import { docsToManifests } from "./k8s-helpers";
11
11
 
12
12
  const PRIVATE_KEY_PATTERN = /-----BEGIN\s+[\w\s]*PRIVATE KEY-----/;
13
13
 
@@ -18,27 +18,22 @@ export const wk8042: PostSynthCheck = {
18
18
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
19
  const diagnostics: PostSynthDiagnostic[] = [];
20
20
 
21
- for (const [, output] of ctx.outputs) {
22
- const yaml = getPrimaryOutput(output);
23
- const manifests = parseK8sManifests(yaml);
24
-
25
- for (const manifest of manifests) {
26
- if (manifest.kind !== "ConfigMap") continue;
27
-
28
- const resourceName = manifest.metadata?.name ?? "ConfigMap";
29
- const data = manifest.data;
30
- if (typeof data !== "object" || data === null) continue;
31
-
32
- for (const [key, value] of Object.entries(data)) {
33
- if (typeof value === "string" && PRIVATE_KEY_PATTERN.test(value)) {
34
- diagnostics.push({
35
- checkId: "WK8042",
36
- severity: "error",
37
- message: `ConfigMap "${resourceName}" contains a private key in data field "${key}" — use a Secret resource instead`,
38
- entity: resourceName,
39
- lexicon: "k8s",
40
- });
41
- }
21
+ for (const manifest of docsToManifests(ctx)) {
22
+ if (manifest.kind !== "ConfigMap") continue;
23
+
24
+ const resourceName = manifest.metadata?.name ?? "ConfigMap";
25
+ const data = manifest.data;
26
+ if (typeof data !== "object" || data === null) continue;
27
+
28
+ for (const [key, value] of Object.entries(data)) {
29
+ if (typeof value === "string" && PRIVATE_KEY_PATTERN.test(value)) {
30
+ diagnostics.push({
31
+ checkId: "WK8042",
32
+ severity: "error",
33
+ message: `ConfigMap "${resourceName}" contains a private key in data field "${key}" use a Secret resource instead`,
34
+ entity: resourceName,
35
+ lexicon: "k8s",
36
+ });
42
37
  }
43
38
  }
44
39
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
- import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
10
+ import { docsToManifests } from "./k8s-helpers";
11
11
 
12
12
  export const wk8101: PostSynthCheck = {
13
13
  id: "WK8101",
@@ -16,46 +16,41 @@ export const wk8101: PostSynthCheck = {
16
16
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
17
  const diagnostics: PostSynthDiagnostic[] = [];
18
18
 
19
- for (const [, output] of ctx.outputs) {
20
- const yaml = getPrimaryOutput(output);
21
- const manifests = parseK8sManifests(yaml);
22
-
23
- for (const manifest of manifests) {
24
- if (manifest.kind !== "Deployment") continue;
25
-
26
- const resourceName = manifest.metadata?.name ?? "Deployment";
27
- const spec = manifest.spec;
28
- if (!spec) continue;
29
-
30
- const selector = spec.selector as Record<string, unknown> | undefined;
31
- const matchLabels = selector?.matchLabels as Record<string, string> | undefined;
32
- if (!matchLabels || typeof matchLabels !== "object") continue;
33
-
34
- const template = spec.template as Record<string, unknown> | undefined;
35
- const templateMetadata = template?.metadata as Record<string, unknown> | undefined;
36
- const templateLabels = templateMetadata?.labels as Record<string, string> | undefined;
19
+ for (const manifest of docsToManifests(ctx)) {
20
+ if (manifest.kind !== "Deployment") continue;
21
+
22
+ const resourceName = manifest.metadata?.name ?? "Deployment";
23
+ const spec = manifest.spec;
24
+ if (!spec) continue;
25
+
26
+ const selector = spec.selector as Record<string, unknown> | undefined;
27
+ const matchLabels = selector?.matchLabels as Record<string, string> | undefined;
28
+ if (!matchLabels || typeof matchLabels !== "object") continue;
29
+
30
+ const template = spec.template as Record<string, unknown> | undefined;
31
+ const templateMetadata = template?.metadata as Record<string, unknown> | undefined;
32
+ const templateLabels = templateMetadata?.labels as Record<string, string> | undefined;
33
+
34
+ if (!templateLabels || typeof templateLabels !== "object") {
35
+ diagnostics.push({
36
+ checkId: "WK8101",
37
+ severity: "error",
38
+ message: `Deployment "${resourceName}" has spec.selector.matchLabels but no spec.template.metadata.labels`,
39
+ entity: resourceName,
40
+ lexicon: "k8s",
41
+ });
42
+ continue;
43
+ }
37
44
 
38
- if (!templateLabels || typeof templateLabels !== "object") {
45
+ for (const [key, value] of Object.entries(matchLabels)) {
46
+ if (templateLabels[key] !== value) {
39
47
  diagnostics.push({
40
48
  checkId: "WK8101",
41
49
  severity: "error",
42
- message: `Deployment "${resourceName}" has spec.selector.matchLabels but no spec.template.metadata.labels`,
50
+ message: `Deployment "${resourceName}" selector label "${key}=${value}" does not match template label "${key}=${templateLabels[key] ?? '(missing)'}"`,
43
51
  entity: resourceName,
44
52
  lexicon: "k8s",
45
53
  });
46
- continue;
47
- }
48
-
49
- for (const [key, value] of Object.entries(matchLabels)) {
50
- if (templateLabels[key] !== value) {
51
- diagnostics.push({
52
- checkId: "WK8101",
53
- severity: "error",
54
- message: `Deployment "${resourceName}" selector label "${key}=${value}" does not match template label "${key}=${templateLabels[key] ?? '(missing)'}"`,
55
- entity: resourceName,
56
- lexicon: "k8s",
57
- });
58
- }
59
54
  }
60
55
  }
61
56
  }