@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
@@ -0,0 +1,36 @@
1
+ /**
2
+ * WK8501: Custom resource spec has a field its CRD schema does not declare
3
+ *
4
+ * A generated CRD class takes `spec: Record<string, unknown>`, so a misspelled
5
+ * field (`classname` for `className`) type-checks and serializes. The API
6
+ * server accepts the object and prunes the unknown field, the controller
7
+ * never sees it, and the resource runs with a default nobody chose. The
8
+ * lexicon ships each CRD's `spec` schema (chant #1372); this check flags any
9
+ * field that schema does not list, unless the enclosing object declares
10
+ * `x-kubernetes-preserve-unknown-fields` or `additionalProperties`.
11
+ */
12
+ import { customResources, resourceLabel, validateSpec } from "./crd-schema-helpers.js";
13
+ export const wk8501 = {
14
+ id: "WK8501",
15
+ description: "Custom resource spec contains a field its CRD schema does not declare",
16
+ check(ctx) {
17
+ const diagnostics = [];
18
+ for (const { manifest, schema } of customResources(ctx)) {
19
+ if (manifest.spec === undefined)
20
+ continue;
21
+ const name = resourceLabel(manifest);
22
+ for (const finding of validateSpec(manifest.spec, schema)) {
23
+ if (finding.kind !== "unknown-field")
24
+ continue;
25
+ diagnostics.push({
26
+ checkId: "WK8501",
27
+ severity: "error",
28
+ message: `${manifest.kind} "${name}": ${finding.message}. The API server prunes it and the controller never sees it.`,
29
+ entity: name,
30
+ lexicon: "k8s",
31
+ });
32
+ }
33
+ }
34
+ return diagnostics;
35
+ },
36
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * WK8502: Custom resource spec field has the wrong type or an invalid enum value
3
+ *
4
+ * With `spec: Record<string, unknown>` on every generated CRD class, nothing
5
+ * at compile time stops `replicas: "2"` or `desiredState: "Runing"`. The API
6
+ * server rejects the first at apply and the controller ignores the second.
7
+ * The lexicon ships each CRD's `spec` schema (chant #1372); this check
8
+ * compares every scalar against its declared type and enum before apply.
9
+ * `x-kubernetes-int-or-string` and untyped nodes accept anything.
10
+ */
11
+ import { customResources, resourceLabel, validateSpec } from "./crd-schema-helpers.js";
12
+ export const wk8502 = {
13
+ id: "WK8502",
14
+ description: "Custom resource spec field has the wrong type or a value outside its enum",
15
+ check(ctx) {
16
+ const diagnostics = [];
17
+ for (const { manifest, schema } of customResources(ctx)) {
18
+ if (manifest.spec === undefined)
19
+ continue;
20
+ const name = resourceLabel(manifest);
21
+ for (const finding of validateSpec(manifest.spec, schema)) {
22
+ if (finding.kind !== "type-mismatch")
23
+ continue;
24
+ diagnostics.push({
25
+ checkId: "WK8502",
26
+ severity: "error",
27
+ message: `${manifest.kind} "${name}": ${finding.message}`,
28
+ entity: name,
29
+ lexicon: "k8s",
30
+ });
31
+ }
32
+ }
33
+ return diagnostics;
34
+ },
35
+ };
@@ -0,0 +1,164 @@
1
+ import { collectSecretDeclarations } from "@intentius/chant/secret-provenance";
2
+ import { extractPodSpec } from "./k8s-helpers.js";
3
+ import { allManifests } from "./argo-helpers.js";
4
+ import { resolveEncryptedSecretClaims } from "./sops-helpers.js";
5
+ function asRecord(value) {
6
+ return typeof value === "object" && value !== null ? value : undefined;
7
+ }
8
+ function asArray(value) {
9
+ return Array.isArray(value) ? value : [];
10
+ }
11
+ /** Collect every Secret the output produces, from literal Secrets and the
12
+ * secret-materializing CR kinds the lexicon ships. */
13
+ function collectProducedSecrets(manifests) {
14
+ const produced = [];
15
+ for (const m of manifests) {
16
+ const metaName = m.metadata?.name;
17
+ const metaNs = m.metadata?.namespace;
18
+ switch (m.kind) {
19
+ case "Secret": {
20
+ if (typeof metaName === "string")
21
+ produced.push({ name: metaName, namespace: metaNs });
22
+ break;
23
+ }
24
+ case "ExternalSecret": {
25
+ // external-secrets.io — writes spec.target.name, defaulting to the CR's own name.
26
+ const target = asRecord(m.spec?.target);
27
+ const targetName = typeof target?.name === "string" ? target.name : metaName;
28
+ if (typeof targetName === "string")
29
+ produced.push({ name: targetName, namespace: metaNs });
30
+ break;
31
+ }
32
+ case "InfisicalSecret":
33
+ case "InfisicalDynamicSecret": {
34
+ // secrets.infisical.com — writes spec.managedSecretReference.secretName;
35
+ // secretNamespace is carried separately from the CR's own namespace.
36
+ const ref = asRecord(m.spec?.managedSecretReference);
37
+ if (typeof ref?.secretName === "string") {
38
+ const ns = typeof ref.secretNamespace === "string" ? ref.secretNamespace : metaNs;
39
+ produced.push({ name: ref.secretName, namespace: ns });
40
+ }
41
+ break;
42
+ }
43
+ case "Certificate": {
44
+ // cert-manager — materializes the keypair into spec.secretName.
45
+ const secretName = m.spec?.secretName;
46
+ if (typeof secretName === "string")
47
+ produced.push({ name: secretName, namespace: metaNs });
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ return produced;
53
+ }
54
+ /** Collect every non-optional secret reference in a pod spec. */
55
+ function collectSecretRefs(podSpec) {
56
+ const refs = [];
57
+ const containers = [
58
+ ...asArray(podSpec.containers),
59
+ ...asArray(podSpec.initContainers),
60
+ ...asArray(podSpec.ephemeralContainers),
61
+ ];
62
+ for (const c of containers) {
63
+ const container = asRecord(c);
64
+ if (!container)
65
+ continue;
66
+ for (const e of asArray(container.envFrom)) {
67
+ const secretRef = asRecord(asRecord(e)?.secretRef);
68
+ if (typeof secretRef?.name === "string" && secretRef.optional !== true) {
69
+ refs.push({ name: secretRef.name, via: "envFrom.secretRef" });
70
+ }
71
+ }
72
+ for (const e of asArray(container.env)) {
73
+ const keyRef = asRecord(asRecord(asRecord(e)?.valueFrom)?.secretKeyRef);
74
+ if (typeof keyRef?.name === "string" && keyRef.optional !== true) {
75
+ refs.push({ name: keyRef.name, via: "env[].valueFrom.secretKeyRef" });
76
+ }
77
+ }
78
+ }
79
+ for (const v of asArray(podSpec.volumes)) {
80
+ const volume = asRecord(v);
81
+ if (!volume)
82
+ continue;
83
+ const secret = asRecord(volume.secret);
84
+ if (typeof secret?.secretName === "string" && secret.optional !== true) {
85
+ refs.push({ name: secret.secretName, via: "volumes[].secret.secretName" });
86
+ }
87
+ const projected = asRecord(volume.projected);
88
+ for (const s of asArray(projected?.sources)) {
89
+ const source = asRecord(asRecord(s)?.secret);
90
+ if (typeof source?.name === "string" && source.optional !== true) {
91
+ refs.push({ name: source.name, via: "volumes[].projected.sources[].secret" });
92
+ }
93
+ }
94
+ }
95
+ for (const p of asArray(podSpec.imagePullSecrets)) {
96
+ const pull = asRecord(p);
97
+ if (typeof pull?.name === "string") {
98
+ refs.push({ name: pull.name, via: "imagePullSecrets" });
99
+ }
100
+ }
101
+ return refs;
102
+ }
103
+ /** True when a produced Secret satisfies a reference. A side with no
104
+ * namespace matches any — only two explicit, different namespaces miss. */
105
+ function producerCovers(producer, name, namespace) {
106
+ if (producer.name !== name)
107
+ return false;
108
+ return producer.namespace === undefined || namespace === undefined || producer.namespace === namespace;
109
+ }
110
+ export const wk8503 = {
111
+ id: "WK8503",
112
+ description: "Workload consumes a Secret nothing in the output produces — produce it (Secret, ExternalSecret, InfisicalSecret, Certificate) or declare its provenance with declareSecret()",
113
+ check(ctx) {
114
+ const diagnostics = [];
115
+ const manifests = allManifests(ctx);
116
+ const produced = collectProducedSecrets(manifests);
117
+ // Committed ciphertext produces a Secret as surely as a Secret manifest
118
+ // does — Flux decrypts the emitted sidecar straight into the cluster — so
119
+ // it joins the producer set, namespace and all. A claim whose file did
120
+ // not resolve produces nothing: WK8504 reports why, and this check still
121
+ // fires, which is the point of not treating it as a waiver.
122
+ for (const claim of resolveEncryptedSecretClaims(ctx)) {
123
+ if (claim.problems.length > 0)
124
+ continue;
125
+ produced.push({ name: claim.declaration.name, namespace: claim.document?.namespace });
126
+ }
127
+ // The typed waiver: a SecretProvenance declaration covering the name, for
128
+ // the three kinds whose value genuinely comes from outside this build.
129
+ const declaredNames = new Set();
130
+ for (const decl of collectSecretDeclarations(ctx.entities).values()) {
131
+ if (decl.provenance === "committed-encrypted")
132
+ continue;
133
+ declaredNames.add(decl.name);
134
+ }
135
+ for (const manifest of manifests) {
136
+ const podSpec = extractPodSpec(manifest);
137
+ if (!podSpec)
138
+ continue;
139
+ const workload = manifest.metadata?.name ?? manifest.kind ?? "workload";
140
+ const namespace = manifest.metadata?.namespace;
141
+ const reported = new Set();
142
+ for (const ref of collectSecretRefs(podSpec)) {
143
+ if (reported.has(ref.name))
144
+ continue;
145
+ if (declaredNames.has(ref.name))
146
+ continue;
147
+ if (produced.some((p) => producerCovers(p, ref.name, namespace)))
148
+ continue;
149
+ reported.add(ref.name);
150
+ diagnostics.push({
151
+ checkId: "WK8503",
152
+ severity: "error",
153
+ message: `${manifest.kind} "${workload}" consumes Secret "${ref.name}" (via ${ref.via}), ` +
154
+ `but nothing in the build output produces it — the pod will fail at start. ` +
155
+ `Produce it in this build, or declare its provenance: ` +
156
+ `declareSecret({ name: "${ref.name}", provenance: "referenced" }).`,
157
+ entity: workload,
158
+ lexicon: "k8s",
159
+ });
160
+ }
161
+ }
162
+ return diagnostics;
163
+ },
164
+ };
@@ -0,0 +1,20 @@
1
+ import { resolveEncryptedSecretClaims } from "./sops-helpers.js";
2
+ export const wk8504 = {
3
+ id: "WK8504",
4
+ description: "committed-encrypted secret declaration does not resolve — the declared file is missing, is not the named Secret, or is not encrypted",
5
+ check(ctx) {
6
+ const diagnostics = [];
7
+ for (const claim of resolveEncryptedSecretClaims(ctx)) {
8
+ for (const problem of claim.problems) {
9
+ diagnostics.push({
10
+ checkId: "WK8504",
11
+ severity: "error",
12
+ message: `declareSecret("${claim.declaration.name}"): ${problem}`,
13
+ entity: claim.declaration.name,
14
+ lexicon: "k8s",
15
+ });
16
+ }
17
+ }
18
+ return diagnostics;
19
+ },
20
+ };
@@ -0,0 +1,31 @@
1
+ import { allManifests, manifestsOfKind } from "./argo-helpers.js";
2
+ import { resolveEncryptedSecretClaims } from "./sops-helpers.js";
3
+ /** Flux Kustomizations only — the kustomize.config.k8s.io kind shares the name. */
4
+ function fluxKustomizations(manifests) {
5
+ return manifestsOfKind(manifests, "Kustomization").filter((m) => {
6
+ const apiVersion = m.apiVersion;
7
+ return typeof apiVersion !== "string" || apiVersion.startsWith("kustomize.toolkit.fluxcd.io/");
8
+ });
9
+ }
10
+ export const wk8505 = {
11
+ id: "WK8505",
12
+ description: "committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it. Sees one build root at a time, so it goes silent when that Kustomization lives in a different build root (chant #1939).",
13
+ check(ctx) {
14
+ const kustomizations = fluxKustomizations(allManifests(ctx));
15
+ if (kustomizations.length === 0)
16
+ return [];
17
+ if (kustomizations.some((k) => k.spec?.decryption !== undefined))
18
+ return [];
19
+ const resolved = resolveEncryptedSecretClaims(ctx).filter((claim) => claim.problems.length === 0);
20
+ return resolved.map((claim) => ({
21
+ checkId: "WK8505",
22
+ severity: "warning",
23
+ message: `declareSecret("${claim.declaration.name}") is committed-encrypted, but no Flux Kustomization ` +
24
+ `in this build sets spec.decryption — Flux will apply "${claim.filename}" without decrypting it. ` +
25
+ `Add decryption: "sops" to the FluxAppFor reconciling the path that carries it, or ignore if that ` +
26
+ `Kustomization is declared in a different build.`,
27
+ entity: claim.declaration.name,
28
+ lexicon: "k8s",
29
+ }));
30
+ },
31
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * WK8601: `tail_sampling` in a collector that runs once per node
3
+ *
4
+ * A DaemonSet collector sees the spans produced on its own node. A trace
5
+ * whose services run on several nodes arrives in pieces, one piece per
6
+ * agent, and `tail_sampling` decides on each piece alone: an error span on
7
+ * node A doesn't keep the rest of the trace on node B, and latency policies
8
+ * measure a fragment. Nothing fails; the sampled traces are just incomplete.
9
+ * Tail sampling belongs on a gateway that receives whole traces (see WK8602
10
+ * for the multi-replica gateway case).
11
+ *
12
+ * Reads the collector config from the ConfigMap the DaemonSet runs, joined
13
+ * through `otel-placement-helpers.ts`. One build root at a time (chant #1939).
14
+ */
15
+ import { docsToManifests } from "./k8s-helpers.js";
16
+ import { collectorPlacements, describePlacement, tracePipelineProcessors } from "./otel-placement-helpers.js";
17
+ export const wk8601 = {
18
+ id: "WK8601",
19
+ description: "OpenTelemetry Collector runs tail_sampling as a per-node DaemonSet; each agent sees only its node's spans, so decisions are made on partial traces. Sees one build root at a time (chant #1939).",
20
+ check(ctx) {
21
+ const diagnostics = [];
22
+ for (const p of collectorPlacements(docsToManifests(ctx))) {
23
+ if (p.kind !== "DaemonSet")
24
+ continue;
25
+ const ids = tracePipelineProcessors(p.config, "tail_sampling");
26
+ if (ids.length === 0)
27
+ continue;
28
+ diagnostics.push({
29
+ checkId: "WK8601",
30
+ severity: "warning",
31
+ message: `${describePlacement(p)} runs ${ids.join(", ")} (${p.where}) on every node. ` +
32
+ `Each agent sees only its own node's spans, so sampling decisions are made on partial traces. ` +
33
+ `Move tail sampling to a gateway (OtelCollectorGateway) and send traces to it through a loadbalancing exporter routed by traceID.`,
34
+ entity: p.name,
35
+ lexicon: "k8s",
36
+ });
37
+ }
38
+ return diagnostics;
39
+ },
40
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * WK8602: `tail_sampling` on a multi-replica gateway that collectors reach
3
+ * without trace-aware load balancing
4
+ *
5
+ * Tail sampling needs every span of a trace in the same process. Behind a
6
+ * ClusterIP Service, connections are spread across the gateway replicas with
7
+ * no regard for trace ids, so the spans of one trace land on different
8
+ * replicas and each samples a fragment. The fix is a `loadbalancing`
9
+ * exporter in front, routing by `traceID` to the replicas themselves (the
10
+ * `k8s` resolver on any Service that selects them, or the `dns` resolver on
11
+ * a headless one). A single-replica gateway passes.
12
+ *
13
+ * The check fires on evidence, not on absence: another collector in the
14
+ * build whose traces pipeline exports to one of the gateway's Services
15
+ * through `otlp`/`otlphttp`, through a `loadbalancing` exporter routed by
16
+ * something other than `traceID`, or through the `dns` resolver on a
17
+ * ClusterIP Service (one virtual IP, so one backend per connection). When
18
+ * the senders live in another build root, or are applications talking to
19
+ * the gateway directly, it stays silent (chant #1939).
20
+ */
21
+ import { canonicalTypeOf } from "@intentius/chant-lexicon-otel/model";
22
+ import { docsToManifests } from "./k8s-helpers.js";
23
+ import { collectorPlacements, describePlacement, hostOfEndpoint, pipelinesOf, serviceOfHost, servicesSelecting, tracePipelineProcessors, } from "./otel-placement-helpers.js";
24
+ const TRACE_ID_KEYS = new Set([undefined, null, "", "traceID"]);
25
+ function record(v) {
26
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
27
+ }
28
+ /** Why each traces exporter of `sender` that reaches `services` does so without trace-aware routing. */
29
+ function unroutedExporters(sender, services) {
30
+ const reasons = [];
31
+ const seen = new Set();
32
+ for (const [, pipeline] of pipelinesOf(sender.config, "traces")) {
33
+ for (const id of pipeline.exporters) {
34
+ if (seen.has(id))
35
+ continue;
36
+ seen.add(id);
37
+ const type = canonicalTypeOf("exporter", id);
38
+ const cfg = record(sender.config.exporters?.[id]);
39
+ if (type === "loadbalancing") {
40
+ const resolver = record(cfg.resolver);
41
+ const k8sService = record(resolver.k8s).service;
42
+ const dnsHost = record(resolver.dns).hostname;
43
+ let target;
44
+ if (typeof k8sService === "string") {
45
+ const svc = serviceOfHost(k8sService, sender.namespace);
46
+ if (svc && services.has(svc))
47
+ target = svc;
48
+ }
49
+ else if (typeof dnsHost === "string") {
50
+ const svc = serviceOfHost(dnsHost, sender.namespace);
51
+ if (svc && services.has(svc)) {
52
+ target = svc;
53
+ if (!services.get(svc)) {
54
+ reasons.push(`${id} resolves ClusterIP Service ${svc} through dns, which returns one virtual IP rather than the replicas`);
55
+ continue;
56
+ }
57
+ }
58
+ }
59
+ if (target && !TRACE_ID_KEYS.has(cfg.routing_key)) {
60
+ reasons.push(`${id} routes by ${String(cfg.routing_key)}, not traceID`);
61
+ }
62
+ continue;
63
+ }
64
+ if (type === "otlp" || type === "otlphttp") {
65
+ for (const field of ["endpoint", "traces_endpoint"]) {
66
+ const host = hostOfEndpoint(cfg[field]);
67
+ const svc = host ? serviceOfHost(host, sender.namespace) : undefined;
68
+ if (svc && services.has(svc)) {
69
+ reasons.push(`${id} sends to Service ${svc} directly`);
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return reasons;
77
+ }
78
+ export const wk8602 = {
79
+ id: "WK8602",
80
+ description: "OpenTelemetry Collector gateway runs tail_sampling on more than one replica, but collectors send it traces without a loadbalancing exporter routed by traceID. Sees one build root at a time (chant #1939).",
81
+ check(ctx) {
82
+ const manifests = docsToManifests(ctx);
83
+ const placements = collectorPlacements(manifests);
84
+ const diagnostics = [];
85
+ for (const gateway of placements) {
86
+ if (gateway.kind === "DaemonSet" || gateway.replicas === undefined || gateway.replicas <= 1)
87
+ continue;
88
+ const tail = tracePipelineProcessors(gateway.config, "tail_sampling");
89
+ if (tail.length === 0)
90
+ continue;
91
+ const services = new Map();
92
+ for (const s of servicesSelecting(manifests, gateway.workload))
93
+ services.set(`${s.name}.${s.namespace}`, s.headless);
94
+ if (services.size === 0)
95
+ continue;
96
+ const senders = new Set();
97
+ for (const sender of placements) {
98
+ if (sender.workload === gateway.workload)
99
+ continue;
100
+ const key = `${sender.kind}/${sender.namespace}/${sender.name}/${sender.configMap}/${sender.key}`;
101
+ if (senders.has(key))
102
+ continue;
103
+ senders.add(key);
104
+ const reasons = unroutedExporters(sender, services);
105
+ if (reasons.length === 0)
106
+ continue;
107
+ diagnostics.push({
108
+ checkId: "WK8602",
109
+ severity: "warning",
110
+ message: `${describePlacement(gateway)} runs ${tail.join(", ")} on up to ${gateway.replicas} replicas, and ${describePlacement(sender)} sends it traces without trace-aware routing: ${reasons.join("; ")}. ` +
111
+ `Spans of one trace land on different replicas and each samples a fragment. ` +
112
+ `Export traces through a loadbalancing exporter with routing_key traceID (gatewayExporter(gateway, { loadBalance: true })), or run one replica.`,
113
+ entity: gateway.name,
114
+ lexicon: "k8s",
115
+ });
116
+ }
117
+ }
118
+ return diagnostics;
119
+ },
120
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * WK8603: `k8s_cluster` receiver in a collector that runs more than one copy
3
+ * without a leader elector
4
+ *
5
+ * The `k8s_cluster` receiver reports cluster-level metrics and entity events
6
+ * (nodes, deployments, pods, resource quotas) by watching the API server.
7
+ * Every copy of the collector reports every object, so in a DaemonSet the
8
+ * cluster is reported once per node, and in a Deployment once per replica:
9
+ * duplicated series and N watches against the API server. Run it in a
10
+ * single-replica Deployment, or set the receiver's `k8s_leader_elector` to a
11
+ * `k8s_leader_elector` extension enabled in `service.extensions` (both ship in
12
+ * otelcol-contrib and otelcol-k8s at the pinned v0.130.0), so only the
13
+ * leader collects.
14
+ *
15
+ * One build root at a time (chant #1939).
16
+ */
17
+ import { docsToManifests } from "./k8s-helpers.js";
18
+ import { allPipelines, collectorPlacements, componentType, describePlacement } from "./otel-placement-helpers.js";
19
+ /** Why a `k8s_cluster` receiver is not behind a working leader elector, or undefined when it is. */
20
+ function missingLeader(config, id) {
21
+ const cfg = config.receivers?.[id];
22
+ const ref = typeof cfg === "object" && cfg !== null ? cfg.k8s_leader_elector : undefined;
23
+ if (typeof ref !== "string" || ref.length === 0)
24
+ return `${id} has no k8s_leader_elector`;
25
+ if (componentType(ref) !== "k8s_leader_elector" || !(config.extensions && ref in config.extensions)) {
26
+ return `${id} names k8s_leader_elector "${ref}", which is not a declared k8s_leader_elector extension`;
27
+ }
28
+ const enabled = Array.isArray(config.service.extensions) && config.service.extensions.includes(ref);
29
+ if (!enabled)
30
+ return `${id} names k8s_leader_elector "${ref}", which is not enabled in service.extensions`;
31
+ return undefined;
32
+ }
33
+ export const wk8603 = {
34
+ id: "WK8603",
35
+ description: "OpenTelemetry Collector runs the k8s_cluster receiver in a DaemonSet or multi-replica workload without a k8s_leader_elector; every copy reports every cluster object. Sees one build root at a time (chant #1939).",
36
+ check(ctx) {
37
+ const diagnostics = [];
38
+ for (const p of collectorPlacements(docsToManifests(ctx))) {
39
+ const copies = p.kind === "DaemonSet" ? "every node" : p.replicas !== undefined && p.replicas > 1 ? `each of up to ${p.replicas} replicas` : undefined;
40
+ if (!copies)
41
+ continue;
42
+ const used = new Set();
43
+ for (const [, pipeline] of allPipelines(p.config)) {
44
+ for (const id of pipeline.receivers)
45
+ if (componentType(id) === "k8s_cluster")
46
+ used.add(id);
47
+ }
48
+ const reasons = [...used].map((id) => missingLeader(p.config, id)).filter((r) => r !== undefined);
49
+ if (reasons.length === 0)
50
+ continue;
51
+ diagnostics.push({
52
+ checkId: "WK8603",
53
+ severity: "warning",
54
+ message: `${describePlacement(p)} runs the k8s_cluster receiver on ${copies} (${p.where}): ${reasons.join("; ")}. ` +
55
+ `Each copy reports every cluster object, duplicating series and API server watches. ` +
56
+ `Run it in a single-replica Deployment, or reference a k8s_leader_elector extension from the receiver and enable it in service.extensions.`,
57
+ entity: p.name,
58
+ lexicon: "k8s",
59
+ });
60
+ }
61
+ return diagnostics;
62
+ },
63
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * WK8604: the otel config checks over collector configs in ConfigMaps
3
+ *
4
+ * `OtelCollector`, `OtelCollectorGateway`, `GkeOtelCollector` and hand-written
5
+ * manifests carry the collector config as a string under a ConfigMap data key.
6
+ * `chant build` gives each lexicon's checks only that lexicon's output, so the
7
+ * otel lexicon's config checks never see a config that lives in the k8s output
8
+ * (chant #2930). This check runs them there: every ConfigMap data value that
9
+ * parses as a collector config (a `service.pipelines` map) goes through the
10
+ * otel lexicon's `validateCollectorConfig`, and through `attributionIssues`
11
+ * (OTEL118) when the build stamps telemetry attribution.
12
+ *
13
+ * An OpenTelemetry Operator `OpenTelemetryCollector` carries its config in
14
+ * `spec.config` (an object in v1beta1, YAML text in v1alpha1), and goes
15
+ * through the same checks; its findings name `OpenTelemetryCollector
16
+ * <namespace>/<name>, spec.config` where a ConfigMap's name the ConfigMap and
17
+ * key (#3367).
18
+ *
19
+ * Findings keep the otel rule ids (OTEL101-OTEL106, OTEL112-OTEL127, and any config
20
+ * check the otel lexicon adds later), so `lint.rules` and suppressions name
21
+ * one id wherever the config lives. Each message names the ConfigMap's
22
+ * namespace, name and key. Needs only the k8s lexicon in the project.
23
+ */
24
+ import { collectorConfigDiagnostics } from "@intentius/chant-lexicon-otel/lint/post-synth/otel-helpers";
25
+ export const wk8604 = {
26
+ id: "WK8604",
27
+ description: "OpenTelemetry Collector config in a ConfigMap or an OpenTelemetryCollector's spec.config fails the otel lexicon's config checks; findings are reported under their OTEL1xx ids, naming the ConfigMap and key, or the OpenTelemetryCollector.",
28
+ check(ctx) {
29
+ return collectorConfigDiagnostics(ctx, { hostedOnly: true });
30
+ },
31
+ };