@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.
- package/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8605: a collector config reads the node, and its workload doesn't give it the node
|
|
3
|
+
*
|
|
4
|
+
* `k8sattributes` filtering to its node by `filter.node_from_env_var`, and
|
|
5
|
+
* `kubeletstats` addressing the kubelet by `${env:K8S_NODE_NAME}`, need that
|
|
6
|
+
* variable set in the pod; unset, the filter matches no pod and the kubelet
|
|
7
|
+
* address doesn't resolve. `hostmetrics` with `root_path` needs the host root
|
|
8
|
+
* mounted there, and `filelog` needs the directories it reads mounted from
|
|
9
|
+
* the host. None of this fails at startup: the collector runs and reports
|
|
10
|
+
* the container instead of the node, or nothing.
|
|
11
|
+
*
|
|
12
|
+
* What a config needs is worked out by `collectorNodeAccess`, the function
|
|
13
|
+
* `OtelCollector` uses to add it (chant #3103), so the composite's own
|
|
14
|
+
* output passes and this fires on hand-written workloads. Only the
|
|
15
|
+
* containers that pass `--config` count (every container when none does). A variable
|
|
16
|
+
* counts as set whatever its source; a mount counts when a hostPath volume
|
|
17
|
+
* covers the path. Whether the container may read root-owned log files is
|
|
18
|
+
* not checked, since that depends on the runtime. One build root at a time
|
|
19
|
+
* (chant #1939).
|
|
20
|
+
*/
|
|
21
|
+
import { docsToManifests, extractContainers, extractPodSpec } from "./k8s-helpers.js";
|
|
22
|
+
import { collectorPlacements, describePlacement, isOperatorCollector } from "./otel-placement-helpers.js";
|
|
23
|
+
import { collectorNodeAccess } from "../../composites/otel-collector-node.js";
|
|
24
|
+
function isRecord(v) {
|
|
25
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
26
|
+
}
|
|
27
|
+
function within(path, dir) {
|
|
28
|
+
const d = dir.replace(/\/+$/, "");
|
|
29
|
+
return d === "" || path === d || path.startsWith(`${d}/`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The containers that run a collector: those passing `--config`, or every
|
|
33
|
+
* container when none does. A sidecar's variables and mounts don't count.
|
|
34
|
+
*/
|
|
35
|
+
function collectorContainers(workload) {
|
|
36
|
+
// An OpenTelemetryCollector's `spec.env` and `spec.volumeMounts` are the collector container's own.
|
|
37
|
+
if (isOperatorCollector(workload))
|
|
38
|
+
return [{ env: workload.spec?.env, volumeMounts: workload.spec?.volumeMounts }];
|
|
39
|
+
const all = extractContainers(workload);
|
|
40
|
+
const withConfig = all.filter((c) => [...(Array.isArray(c.command) ? c.command : []), ...(Array.isArray(c.args) ? c.args : [])].some((a) => typeof a === "string" && (a === "--config" || a.startsWith("--config="))));
|
|
41
|
+
return withConfig.length > 0 ? withConfig : all;
|
|
42
|
+
}
|
|
43
|
+
/** Each container mount backed by a hostPath volume, as [host path, container path]. */
|
|
44
|
+
function hostMounts(workload, containers) {
|
|
45
|
+
const pod = isOperatorCollector(workload) ? workload.spec : extractPodSpec(workload);
|
|
46
|
+
const hostPaths = new Map();
|
|
47
|
+
for (const v of Array.isArray(pod?.volumes) ? pod.volumes : []) {
|
|
48
|
+
if (isRecord(v) && typeof v.name === "string" && isRecord(v.hostPath) && typeof v.hostPath.path === "string") {
|
|
49
|
+
hostPaths.set(v.name, v.hostPath.path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const c of containers) {
|
|
54
|
+
for (const m of Array.isArray(c.volumeMounts) ? c.volumeMounts : []) {
|
|
55
|
+
if (!isRecord(m) || typeof m.name !== "string" || typeof m.mountPath !== "string")
|
|
56
|
+
continue;
|
|
57
|
+
const host = hostPaths.get(m.name);
|
|
58
|
+
if (host !== undefined)
|
|
59
|
+
out.push([host.replace(/\/+$/, "") || "/", m.mountPath.replace(/\/+$/, "") || "/"]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Whether some hostPath mount gives the container the host path a requirement names, at its path. */
|
|
65
|
+
function covered(need, mounts) {
|
|
66
|
+
return mounts.some(([host, path]) => {
|
|
67
|
+
// hostmetrics takes the host root or parts of it anywhere at or under root_path.
|
|
68
|
+
if (need.partial && within(path, need.mountPath)) {
|
|
69
|
+
const rel = path.slice(need.mountPath.length);
|
|
70
|
+
return host === (rel || "/");
|
|
71
|
+
}
|
|
72
|
+
if (!within(need.mountPath, path))
|
|
73
|
+
return false;
|
|
74
|
+
const rel = need.mountPath.slice(path === "/" ? 0 : path.length);
|
|
75
|
+
const expected = `${host === "/" ? "" : host}${rel}` || "/";
|
|
76
|
+
return expected === need.hostPath;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function envNames(containers) {
|
|
80
|
+
const names = new Set();
|
|
81
|
+
for (const c of containers) {
|
|
82
|
+
for (const e of Array.isArray(c.env) ? c.env : []) {
|
|
83
|
+
if (isRecord(e) && typeof e.name === "string")
|
|
84
|
+
names.add(e.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return names;
|
|
88
|
+
}
|
|
89
|
+
export const wk8605 = {
|
|
90
|
+
id: "WK8605",
|
|
91
|
+
description: "OpenTelemetry Collector config reads the node (k8sattributes filtered to its node, kubeletstats, hostmetrics root_path, filelog) but its workload doesn't set the node name variable or mount the host paths. Sees one build root at a time (chant #1939).",
|
|
92
|
+
check(ctx) {
|
|
93
|
+
const diagnostics = [];
|
|
94
|
+
for (const p of collectorPlacements(docsToManifests(ctx))) {
|
|
95
|
+
const need = collectorNodeAccess(p.config);
|
|
96
|
+
if (need.env.length === 0 && need.mounts.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
const containers = collectorContainers(p.workload);
|
|
99
|
+
const env = envNames(containers);
|
|
100
|
+
const mounts = hostMounts(p.workload, containers);
|
|
101
|
+
const gaps = [];
|
|
102
|
+
for (const v of need.env) {
|
|
103
|
+
if (!env.has(v.name))
|
|
104
|
+
gaps.push(`no ${v.name} variable (set it from ${v.valueFrom.fieldRef.fieldPath} with the downward API)`);
|
|
105
|
+
}
|
|
106
|
+
for (const m of need.mounts) {
|
|
107
|
+
if (!covered(m, mounts))
|
|
108
|
+
gaps.push(`${m.component} reads host ${m.hostPath} at ${m.mountPath}, which no hostPath volume mounts`);
|
|
109
|
+
}
|
|
110
|
+
if (gaps.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
diagnostics.push({
|
|
113
|
+
checkId: "WK8605",
|
|
114
|
+
severity: "warning",
|
|
115
|
+
message: `${describePlacement(p)} runs a collector config that reads the node (${p.where}), but: ${gaps.join("; ")}. ` +
|
|
116
|
+
`Without them the collector reports on its own container, or nothing. OtelCollector adds these from the config; ` +
|
|
117
|
+
`in a hand-written workload, add them read-only.`,
|
|
118
|
+
entity: p.name,
|
|
119
|
+
lexicon: "k8s",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return diagnostics;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8606: the prometheus rule checks over PrometheusRule groups
|
|
3
|
+
*
|
|
4
|
+
* `PrometheusRule.spec.groups` is a rule file's `groups`. `chant build` gives
|
|
5
|
+
* each lexicon's checks only that lexicon's output, so the prometheus
|
|
6
|
+
* lexicon's PROM checks never see a rule that lives in the k8s output. This
|
|
7
|
+
* check runs them there: every PrometheusRule's groups go through the
|
|
8
|
+
* prometheus lexicon's rule-file checks (PROM101-PROM107, PROM211,
|
|
9
|
+
* PROM213-PROM219; PROM212 stays opt-in).
|
|
10
|
+
*
|
|
11
|
+
* Findings keep the PROM ids, so `lint.rules` and suppressions name one id
|
|
12
|
+
* wherever the rules live. Each message names the PrometheusRule's namespace
|
|
13
|
+
* and name. Needs only the k8s lexicon in the project.
|
|
14
|
+
*/
|
|
15
|
+
import { prometheusRuleDiagnostics } from "@intentius/chant-lexicon-prometheus/lint/post-synth/prom-helpers";
|
|
16
|
+
export const wk8606 = {
|
|
17
|
+
id: "WK8606",
|
|
18
|
+
description: "A PrometheusRule's groups fail the prometheus lexicon's rule-file checks; findings are reported under their PROM ids, naming the PrometheusRule.",
|
|
19
|
+
check(ctx) {
|
|
20
|
+
return prometheusRuleDiagnostics(ctx);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8701: a PrometheusRule that no Prometheus or ThanosRuler selects
|
|
3
|
+
*
|
|
4
|
+
* A Prometheus or ThanosRuler runs the rules whose labels match its `ruleSelector` in the namespaces its
|
|
5
|
+
* `ruleNamespaceSelector` picks. A PrometheusRule neither selects is accepted by the API server and then
|
|
6
|
+
* never evaluated: no error, no alert.
|
|
7
|
+
*
|
|
8
|
+
* Selection is worked out from the manifests in one build root (chant #1939),
|
|
9
|
+
* by the operator's rules: a null object selector matches nothing, `{}`
|
|
10
|
+
* matches all; a null namespace selector matches only the selecting resource's
|
|
11
|
+
* own namespace, `{}` every namespace. Silent when the build has no Prometheus or ThanosRuler, since the
|
|
12
|
+
* stack is often installed separately. A selector on a namespace label the
|
|
13
|
+
* build cannot read (the Namespace is not declared in it) counts as selecting.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, monitoringKind, nameOf, selectionContext, unselected } from "./monitoring-selection-helpers.js";
|
|
16
|
+
export const wk8701 = {
|
|
17
|
+
id: "WK8701",
|
|
18
|
+
description: "A PrometheusRule that no Prometheus or ThanosRuler in the build selects through ruleSelector and ruleNamespaceSelector is never evaluated. A null selector matches nothing, {} matches all. Silent when the build has no Prometheus or ThanosRuler.",
|
|
19
|
+
check(ctx) {
|
|
20
|
+
const sc = selectionContext(ctx);
|
|
21
|
+
const selectors = [...monitoringKind(sc.manifests, "Prometheus"), ...monitoringKind(sc.manifests, "ThanosRuler")];
|
|
22
|
+
return unselected(sc, monitoringKind(sc.manifests, "PrometheusRule"), selectors, () => ["ruleSelector", "ruleNamespaceSelector"]).map((r) => ({
|
|
23
|
+
checkId: "WK8701",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `${describe(r)} is selected by no Prometheus or ThanosRuler in this build, so the operator never loads it and its rules are never evaluated. ` +
|
|
26
|
+
`Match its labels (and namespace) with ruleSelector and ruleNamespaceSelector: null selects nothing, {} selects all.`,
|
|
27
|
+
entity: nameOf(r),
|
|
28
|
+
lexicon: "k8s",
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8702: a ServiceMonitor, PodMonitor, Probe or ScrapeConfig that no Prometheus or PrometheusAgent selects
|
|
3
|
+
*
|
|
4
|
+
* A Prometheus or PrometheusAgent scrapes the monitors whose labels match its `serviceMonitorSelector`,
|
|
5
|
+
* `podMonitorSelector`, `probeSelector` or `scrapeConfigSelector`, in the namespaces the matching
|
|
6
|
+
* `*NamespaceSelector` picks. A monitor none selects is accepted and then never scraped.
|
|
7
|
+
*
|
|
8
|
+
* Selection is worked out from the manifests in one build root (chant #1939),
|
|
9
|
+
* by the operator's rules: a null object selector matches nothing, `{}`
|
|
10
|
+
* matches all; a null namespace selector matches only the selecting resource's
|
|
11
|
+
* own namespace, `{}` every namespace. Silent when the build has no Prometheus or PrometheusAgent, since the
|
|
12
|
+
* stack is often installed separately. A selector on a namespace label the
|
|
13
|
+
* build cannot read (the Namespace is not declared in it) counts as selecting.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, monitoringKind, nameOf, selectionContext, unselected } from "./monitoring-selection-helpers.js";
|
|
16
|
+
export const wk8702 = {
|
|
17
|
+
id: "WK8702",
|
|
18
|
+
description: "A ServiceMonitor, PodMonitor, Probe or ScrapeConfig that no Prometheus or PrometheusAgent in the build selects is never scraped. A null selector matches nothing, {} matches all. Silent when the build has no Prometheus or PrometheusAgent.",
|
|
19
|
+
check(ctx) {
|
|
20
|
+
const sc = selectionContext(ctx);
|
|
21
|
+
const selectors = [...monitoringKind(sc.manifests, "Prometheus"), ...monitoringKind(sc.manifests, "PrometheusAgent")];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const [kind, prefix] of [["ServiceMonitor", "serviceMonitor"], ["PodMonitor", "podMonitor"], ["Probe", "probe"], ["ScrapeConfig", "scrapeConfig"]]) {
|
|
24
|
+
for (const m of unselected(sc, monitoringKind(sc.manifests, kind), selectors, () => [`${prefix}Selector`, `${prefix}NamespaceSelector`])) {
|
|
25
|
+
out.push({
|
|
26
|
+
checkId: "WK8702",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `${describe(m)} is selected by no Prometheus or PrometheusAgent in this build, so it is never scraped. ` +
|
|
29
|
+
`Match its labels (and namespace) with ${prefix}Selector and ${prefix}NamespaceSelector: null selects nothing, {} selects all.`,
|
|
30
|
+
entity: nameOf(m),
|
|
31
|
+
lexicon: "k8s",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8703: an AlertmanagerConfig that no Alertmanager selects
|
|
3
|
+
*
|
|
4
|
+
* An Alertmanager merges the AlertmanagerConfigs whose labels match its `alertmanagerConfigSelector` in the
|
|
5
|
+
* namespaces its `alertmanagerConfigNamespaceSelector` picks. One neither selects is accepted and then
|
|
6
|
+
* never routes an alert.
|
|
7
|
+
*
|
|
8
|
+
* Selection is worked out from the manifests in one build root (chant #1939),
|
|
9
|
+
* by the operator's rules: a null object selector matches nothing, `{}`
|
|
10
|
+
* matches all; a null namespace selector matches only the selecting resource's
|
|
11
|
+
* own namespace, `{}` every namespace. Silent when the build has no Alertmanager, since the
|
|
12
|
+
* stack is often installed separately. A selector on a namespace label the
|
|
13
|
+
* build cannot read (the Namespace is not declared in it) counts as selecting.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, monitoringKind, nameOf, selectionContext, unselected } from "./monitoring-selection-helpers.js";
|
|
16
|
+
export const wk8703 = {
|
|
17
|
+
id: "WK8703",
|
|
18
|
+
description: "An AlertmanagerConfig that no Alertmanager in the build selects through alertmanagerConfigSelector and alertmanagerConfigNamespaceSelector never routes an alert. A null selector matches nothing, {} matches all. Silent when the build has no Alertmanager.",
|
|
19
|
+
check(ctx) {
|
|
20
|
+
const sc = selectionContext(ctx);
|
|
21
|
+
const configs = monitoringKind(sc.manifests, "AlertmanagerConfig");
|
|
22
|
+
return unselected(sc, configs, monitoringKind(sc.manifests, "Alertmanager"), () => ["alertmanagerConfigSelector", "alertmanagerConfigNamespaceSelector"]).map((c) => ({
|
|
23
|
+
checkId: "WK8703",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `${describe(c)} is selected by no Alertmanager in this build, so its routes and receivers are never used. ` +
|
|
26
|
+
`Match its labels (and namespace) with alertmanagerConfigSelector and alertmanagerConfigNamespaceSelector: null selects nothing, {} selects all.`,
|
|
27
|
+
entity: nameOf(c),
|
|
28
|
+
lexicon: "k8s",
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8704: a ServiceMonitor or PodMonitor that selects no Service or pod, or names a port that is not there
|
|
3
|
+
*
|
|
4
|
+
* A ServiceMonitor's `selector` picks Services (in its own namespace, the
|
|
5
|
+
* `namespaceSelector.matchNames` ones, or every namespace with `any: true`),
|
|
6
|
+
* and each `endpoints[].port` names a port on them. A PodMonitor's `selector`
|
|
7
|
+
* picks pods, and each `podMetricsEndpoints[].port` names a container port.
|
|
8
|
+
* When nothing matches, or the port name is on none of the matches, Prometheus
|
|
9
|
+
* scrapes nothing and says nothing. This check needs no Prometheus in the
|
|
10
|
+
* build: it reads the monitor against the Services and workloads declared
|
|
11
|
+
* beside it.
|
|
12
|
+
*
|
|
13
|
+
* Pods come from the pod templates of Deployment, StatefulSet, DaemonSet, Job
|
|
14
|
+
* and CronJob, and from Pods themselves. An empty `selector` ({}) matches
|
|
15
|
+
* everything, as the operator does. Only a port given by name is checked;
|
|
16
|
+
* `targetPort` and `portNumber` are left alone. One build root at a time
|
|
17
|
+
* (chant #1939): a Service or workload installed from another root is not seen.
|
|
18
|
+
*/
|
|
19
|
+
import { extractContainers } from "./k8s-helpers.js";
|
|
20
|
+
import { describe, labelsOf, matchLabelSelector, monitoringKind, nameOf, namespaceOf, selectionContext } from "./monitoring-selection-helpers.js";
|
|
21
|
+
function isRecord(v) {
|
|
22
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23
|
+
}
|
|
24
|
+
/** Whether a monitor looks in this namespace: its own, `matchNames`, or all of them with `any`. */
|
|
25
|
+
function reaches(monitor, namespace) {
|
|
26
|
+
const ns = monitor.spec?.namespaceSelector;
|
|
27
|
+
if (isRecord(ns)) {
|
|
28
|
+
if (ns.any === true)
|
|
29
|
+
return true;
|
|
30
|
+
if (Array.isArray(ns.matchNames) && ns.matchNames.length > 0)
|
|
31
|
+
return ns.matchNames.includes(namespace);
|
|
32
|
+
}
|
|
33
|
+
return namespaceOf(monitor) === namespace;
|
|
34
|
+
}
|
|
35
|
+
/** The labels a pod from this workload carries, and its manifest to read ports from. */
|
|
36
|
+
function podsOf(manifests) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const m of manifests) {
|
|
39
|
+
const spec = (m.spec ?? {});
|
|
40
|
+
if (m.kind === "Pod") {
|
|
41
|
+
out.push({ manifest: m, labels: labelsOf(m) });
|
|
42
|
+
}
|
|
43
|
+
else if (m.kind === "Deployment" || m.kind === "StatefulSet" || m.kind === "DaemonSet" || m.kind === "Job") {
|
|
44
|
+
out.push({ manifest: m, labels: labelsOf({ metadata: spec.template?.metadata }) });
|
|
45
|
+
}
|
|
46
|
+
else if (m.kind === "CronJob") {
|
|
47
|
+
out.push({ manifest: m, labels: labelsOf({ metadata: spec.jobTemplate?.spec?.template?.metadata }) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function containerPortNames(workload) {
|
|
53
|
+
const names = new Set();
|
|
54
|
+
for (const c of extractContainers(workload)) {
|
|
55
|
+
for (const p of Array.isArray(c.ports) ? c.ports : []) {
|
|
56
|
+
if (isRecord(p) && typeof p.name === "string")
|
|
57
|
+
names.add(p.name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
62
|
+
function servicePortNames(service) {
|
|
63
|
+
const names = new Set();
|
|
64
|
+
const ports = service.spec?.ports;
|
|
65
|
+
for (const p of Array.isArray(ports) ? ports : []) {
|
|
66
|
+
if (isRecord(p) && typeof p.name === "string")
|
|
67
|
+
names.add(p.name);
|
|
68
|
+
}
|
|
69
|
+
return names;
|
|
70
|
+
}
|
|
71
|
+
export const wk8704 = {
|
|
72
|
+
id: "WK8704",
|
|
73
|
+
description: "A ServiceMonitor whose selector matches no Service in the build, or whose endpoints[].port names no port on the matched Services; the same for a PodMonitor against pod container ports. Needs no Prometheus in the build. Sees one build root at a time (chant #1939).",
|
|
74
|
+
check(ctx) {
|
|
75
|
+
const { manifests } = selectionContext(ctx);
|
|
76
|
+
const out = [];
|
|
77
|
+
const diag = (m, message) => ({
|
|
78
|
+
checkId: "WK8704",
|
|
79
|
+
severity: "warning",
|
|
80
|
+
message,
|
|
81
|
+
entity: nameOf(m),
|
|
82
|
+
lexicon: "k8s",
|
|
83
|
+
});
|
|
84
|
+
const services = manifests.filter((m) => m.kind === "Service" && (m.apiVersion === "v1" || m.apiVersion === undefined));
|
|
85
|
+
for (const sm of monitoringKind(manifests, "ServiceMonitor")) {
|
|
86
|
+
const spec = (sm.spec ?? {});
|
|
87
|
+
// A ServiceMonitor's selector is required; treat a missing one like {}.
|
|
88
|
+
const selector = isRecord(spec.selector) ? spec.selector : {};
|
|
89
|
+
const matched = services.filter((s) => reaches(sm, namespaceOf(s)) && matchLabelSelector(selector, labelsOf(s)) !== "no");
|
|
90
|
+
if (matched.length === 0) {
|
|
91
|
+
out.push(diag(sm, `${describe(sm)} has a selector that matches no Service in this build (looking in ${where(sm)}), so Prometheus scrapes nothing for it. Make spec.selector match the labels on the Service, or fix namespaceSelector.`));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const known = new Set();
|
|
95
|
+
for (const s of matched)
|
|
96
|
+
for (const n of servicePortNames(s))
|
|
97
|
+
known.add(n);
|
|
98
|
+
for (const e of Array.isArray(spec.endpoints) ? spec.endpoints : []) {
|
|
99
|
+
if (!isRecord(e) || typeof e.port !== "string" || known.has(e.port))
|
|
100
|
+
continue;
|
|
101
|
+
out.push(diag(sm, `${describe(sm)} scrapes endpoint port "${e.port}", but the Service(s) it selects (${matched.map(nameOf).join(", ")}) declare no port with that name${known.size > 0 ? ` (they have: ${[...known].join(", ")})` : ""}. Name the Service port, or point the endpoint at an existing one.`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const pods = podsOf(manifests);
|
|
105
|
+
for (const pm of monitoringKind(manifests, "PodMonitor")) {
|
|
106
|
+
const spec = (pm.spec ?? {});
|
|
107
|
+
const selector = isRecord(spec.selector) ? spec.selector : {};
|
|
108
|
+
const matched = pods.filter((p) => reaches(pm, namespaceOf(p.manifest)) && matchLabelSelector(selector, p.labels) !== "no");
|
|
109
|
+
if (matched.length === 0) {
|
|
110
|
+
out.push(diag(pm, `${describe(pm)} has a selector that matches no pod in this build (looking in ${where(pm)}), so Prometheus scrapes nothing for it. Make spec.selector match the pod template labels, or fix namespaceSelector.`));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const known = new Set();
|
|
114
|
+
for (const p of matched)
|
|
115
|
+
for (const n of containerPortNames(p.manifest))
|
|
116
|
+
known.add(n);
|
|
117
|
+
for (const e of Array.isArray(spec.podMetricsEndpoints) ? spec.podMetricsEndpoints : []) {
|
|
118
|
+
if (!isRecord(e) || typeof e.port !== "string" || known.has(e.port))
|
|
119
|
+
continue;
|
|
120
|
+
out.push(diag(pm, `${describe(pm)} scrapes container port "${e.port}", but the pods it selects (${matched.map((p) => nameOf(p.manifest)).join(", ")}) declare no container port with that name${known.size > 0 ? ` (they have: ${[...known].join(", ")})` : ""}. Name the container port, or point the endpoint at an existing one.`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
function where(m) {
|
|
127
|
+
const ns = m.spec?.namespaceSelector;
|
|
128
|
+
if (isRecord(ns)) {
|
|
129
|
+
if (ns.any === true)
|
|
130
|
+
return "every namespace";
|
|
131
|
+
if (Array.isArray(ns.matchNames) && ns.matchNames.length > 0)
|
|
132
|
+
return `namespace ${ns.matchNames.join(", ")}`;
|
|
133
|
+
}
|
|
134
|
+
return `namespace ${namespaceOf(m)}`;
|
|
135
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8705: a Prometheus whose four monitor selectors are all null
|
|
3
|
+
*
|
|
4
|
+
* A Prometheus picks the ServiceMonitors, PodMonitors, Probes and
|
|
5
|
+
* ScrapeConfigs to scrape with `serviceMonitorSelector`, `podMonitorSelector`,
|
|
6
|
+
* `probeSelector` and `scrapeConfigSelector`. A null selector (absent counts
|
|
7
|
+
* as null) matches nothing, so a Prometheus with all four null selects no
|
|
8
|
+
* monitor at all and scrapes only what `additionalScrapeConfigs` and its own
|
|
9
|
+
* config hold. `{}` matches every monitor in the selected namespaces. This is
|
|
10
|
+
* the usual way a fresh Prometheus ends up scraping nothing.
|
|
11
|
+
*
|
|
12
|
+
* A Prometheus with `additionalScrapeConfigs` set still reports: that
|
|
13
|
+
* is a deliberate setup the finding names, not one it hides. The message says
|
|
14
|
+
* so, and a suppression is the way to accept it.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, monitoringKind, nameOf, selectionContext } from "./monitoring-selection-helpers.js";
|
|
17
|
+
const SELECTORS = ["serviceMonitorSelector", "podMonitorSelector", "probeSelector", "scrapeConfigSelector"];
|
|
18
|
+
export const wk8705 = {
|
|
19
|
+
id: "WK8705",
|
|
20
|
+
description: "A Prometheus whose serviceMonitorSelector, podMonitorSelector, probeSelector and scrapeConfigSelector are all null selects no monitor, so the operator scrapes nothing from ServiceMonitors, PodMonitors, Probes or ScrapeConfigs.",
|
|
21
|
+
check(ctx) {
|
|
22
|
+
const { manifests } = selectionContext(ctx);
|
|
23
|
+
return monitoringKind(manifests, "Prometheus")
|
|
24
|
+
.filter((p) => {
|
|
25
|
+
const spec = (p.spec ?? {});
|
|
26
|
+
return SELECTORS.every((k) => spec[k] === null || spec[k] === undefined);
|
|
27
|
+
})
|
|
28
|
+
.map((p) => ({
|
|
29
|
+
checkId: "WK8705",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
message: `${describe(p)} has ${SELECTORS.join(", ")} all null, so it selects no ServiceMonitor, PodMonitor, Probe or ScrapeConfig and scrapes only its own config` +
|
|
32
|
+
`${p.spec?.additionalScrapeConfigs ? " and additionalScrapeConfigs" : ""}. ` +
|
|
33
|
+
`Set the selector for each monitor kind it should pick up ({} selects all in the selected namespaces).`,
|
|
34
|
+
entity: nameOf(p),
|
|
35
|
+
lexicon: "k8s",
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { findResourceLiterals, getNestedObject, getNestedString, lineCol } from "./argo-ast.js";
|
|
2
|
+
/**
|
|
3
|
+
* ARGO004: ApplicationSet template must scope to a single AppProject
|
|
4
|
+
*
|
|
5
|
+
* An `ApplicationSet` generates many Applications from one template. The
|
|
6
|
+
* template's `spec.project` should name a single, static `AppProject` so every
|
|
7
|
+
* generated Application lands in the same security boundary. If `project` is
|
|
8
|
+
* missing, the generated Applications fall back to whatever default and dodge
|
|
9
|
+
* project-level RBAC; if it is templated with a generator placeholder
|
|
10
|
+
* (`{{...}}`) the set sprays Applications across many projects, which defeats
|
|
11
|
+
* the point of an AppProject as a guardrail.
|
|
12
|
+
*
|
|
13
|
+
* Bad: new ApplicationSet({ spec: { template: { spec: { repoURL: "..." } } } }) // no project
|
|
14
|
+
* Bad: new ApplicationSet({ spec: { template: { spec: { project: "{{path.basename}}" } } } }) // templated
|
|
15
|
+
* Good: new ApplicationSet({ spec: { template: { spec: { project: "team-a" } } } })
|
|
16
|
+
*/
|
|
17
|
+
export const argoAppSetSingleProjectRule = {
|
|
18
|
+
id: "ARGO004",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
category: "correctness",
|
|
21
|
+
description: "ApplicationSet template must scope to a single static AppProject (spec.template.spec.project)",
|
|
22
|
+
check(context) {
|
|
23
|
+
const { sourceFile } = context;
|
|
24
|
+
const diagnostics = [];
|
|
25
|
+
for (const { literal } of findResourceLiterals(sourceFile, new Set(["ApplicationSet"]))) {
|
|
26
|
+
const templateSpec = getNestedObject(literal, ["spec", "template", "spec"]);
|
|
27
|
+
// No template/spec to inspect — leave it to other tooling.
|
|
28
|
+
if (!templateSpec)
|
|
29
|
+
continue;
|
|
30
|
+
const project = getNestedString(literal, ["spec", "template", "spec", "project"]);
|
|
31
|
+
const { line, column } = lineCol(sourceFile, templateSpec);
|
|
32
|
+
if (project === undefined) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
file: sourceFile.fileName,
|
|
35
|
+
line,
|
|
36
|
+
column,
|
|
37
|
+
ruleId: "ARGO004",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message: "ApplicationSet template has no spec.project — scope it to a single AppProject so every generated Application inherits the same RBAC boundary.",
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (project.includes("{{")) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
file: sourceFile.fileName,
|
|
46
|
+
line,
|
|
47
|
+
column,
|
|
48
|
+
ruleId: "ARGO004",
|
|
49
|
+
severity: "warning",
|
|
50
|
+
message: `ApplicationSet template scopes spec.project to a generator placeholder ("${project}"), spraying Applications across projects. Pin it to a single static AppProject.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return diagnostics;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST helpers for the Argo declarative lint rules (ARGO001, ARGO004).
|
|
3
|
+
*
|
|
4
|
+
* The Argo CRDs are constructed like any other k8s resource —
|
|
5
|
+
* `new Application({ metadata, spec })` / `new ApplicationSet({ ... })`. These
|
|
6
|
+
* helpers walk the object-literal first argument so a rule can read a nested
|
|
7
|
+
* property by path without re-implementing the traversal each time.
|
|
8
|
+
*/
|
|
9
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
10
|
+
/**
|
|
11
|
+
* Find the object-literal first argument of every `new <Kind>(...)` expression
|
|
12
|
+
* in the file, where Kind is one of the supplied resource kinds.
|
|
13
|
+
*/
|
|
14
|
+
export function findResourceLiterals(sourceFile, kinds) {
|
|
15
|
+
const found = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isNewExpression(node) &&
|
|
18
|
+
ts.isIdentifier(node.expression) &&
|
|
19
|
+
kinds.has(node.expression.text) &&
|
|
20
|
+
node.arguments &&
|
|
21
|
+
node.arguments.length > 0 &&
|
|
22
|
+
ts.isObjectLiteralExpression(node.arguments[0])) {
|
|
23
|
+
found.push({
|
|
24
|
+
kind: node.expression.text,
|
|
25
|
+
literal: node.arguments[0],
|
|
26
|
+
node,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
ts.forEachChild(node, visit);
|
|
30
|
+
}
|
|
31
|
+
visit(sourceFile);
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
/** Read the initializer node for `key` on an object literal, if present. */
|
|
35
|
+
export function getProp(obj, key) {
|
|
36
|
+
for (const prop of obj.properties) {
|
|
37
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
38
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
39
|
+
prop.name.text === key) {
|
|
40
|
+
return prop.initializer;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Read a nested object literal by walking `path` (each segment an object). */
|
|
46
|
+
export function getNestedObject(obj, path) {
|
|
47
|
+
let current = obj;
|
|
48
|
+
for (const segment of path) {
|
|
49
|
+
if (!current)
|
|
50
|
+
return undefined;
|
|
51
|
+
const next = getProp(current, segment);
|
|
52
|
+
current = next && ts.isObjectLiteralExpression(next) ? next : undefined;
|
|
53
|
+
}
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
/** Read a string-literal property value by path (last segment is the key). */
|
|
57
|
+
export function getNestedString(obj, path) {
|
|
58
|
+
const key = path[path.length - 1];
|
|
59
|
+
const parent = getNestedObject(obj, path.slice(0, -1));
|
|
60
|
+
if (!parent)
|
|
61
|
+
return undefined;
|
|
62
|
+
const value = getProp(parent, key);
|
|
63
|
+
return value && ts.isStringLiteral(value) ? value.text : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Read a boolean-literal property value by path (last segment is the key). */
|
|
66
|
+
export function getNestedBoolean(obj, path) {
|
|
67
|
+
const key = path[path.length - 1];
|
|
68
|
+
const parent = getNestedObject(obj, path.slice(0, -1));
|
|
69
|
+
if (!parent)
|
|
70
|
+
return undefined;
|
|
71
|
+
const value = getProp(parent, key);
|
|
72
|
+
if (!value)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
75
|
+
return true;
|
|
76
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
77
|
+
return false;
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* True if the literal carries the given annotation key under
|
|
82
|
+
* `metadata.annotations`, regardless of value.
|
|
83
|
+
*/
|
|
84
|
+
export function hasAnnotation(obj, annotationKey) {
|
|
85
|
+
const annotations = getNestedObject(obj, ["metadata", "annotations"]);
|
|
86
|
+
if (!annotations)
|
|
87
|
+
return false;
|
|
88
|
+
return getProp(annotations, annotationKey) !== undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Line/column (1-based) for a node, for diagnostics. */
|
|
91
|
+
export function lineCol(sourceFile, node) {
|
|
92
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
93
|
+
return { line: line + 1, column: character + 1 };
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { findResourceLiterals, getNestedBoolean, getNestedString, getProp, hasAnnotation, lineCol, } from "./argo-ast.js";
|
|
2
|
+
/**
|
|
3
|
+
* ARGO001: Production Application must not enable automated prune
|
|
4
|
+
*
|
|
5
|
+
* An Argo CD `Application` whose `syncPolicy.automated.prune` is `true` lets the
|
|
6
|
+
* controller delete live resources that disappear from git. On a production
|
|
7
|
+
* Application that is a foot-gun — a bad merge can sweep away running
|
|
8
|
+
* infrastructure. Require `prune: false` for prod Applications unless the author
|
|
9
|
+
* opts in explicitly with the `argocd.chant.dev/allow-prune` annotation.
|
|
10
|
+
*
|
|
11
|
+
* "Production" is inferred from the Application name, its `metadata.namespace`,
|
|
12
|
+
* or its `spec.destination.namespace` containing `prod`.
|
|
13
|
+
*
|
|
14
|
+
* Bad: new Application({ metadata: { name: "api-prod" }, spec: { syncPolicy: { automated: { prune: true } } } })
|
|
15
|
+
* Good: new Application({ metadata: { name: "api-prod" }, spec: { syncPolicy: { automated: { prune: false } } } })
|
|
16
|
+
* Good: new Application({ metadata: { name: "api-prod", annotations: { "argocd.chant.dev/allow-prune": "true" } }, spec: { syncPolicy: { automated: { prune: true } } } })
|
|
17
|
+
*/
|
|
18
|
+
export const ALLOW_PRUNE_ANNOTATION = "argocd.chant.dev/allow-prune";
|
|
19
|
+
function looksProd(obj) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
getNestedString(obj, ["metadata", "name"]),
|
|
22
|
+
getNestedString(obj, ["metadata", "namespace"]),
|
|
23
|
+
getNestedString(obj, ["spec", "destination", "namespace"]),
|
|
24
|
+
];
|
|
25
|
+
return candidates.some((v) => v !== undefined && /prod/i.test(v));
|
|
26
|
+
}
|
|
27
|
+
export const argoAutomatedPruneRule = {
|
|
28
|
+
id: "ARGO001",
|
|
29
|
+
severity: "warning",
|
|
30
|
+
category: "correctness",
|
|
31
|
+
description: "Production Argo Application must not enable syncPolicy.automated.prune without the allow-prune override annotation",
|
|
32
|
+
check(context) {
|
|
33
|
+
const { sourceFile } = context;
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
for (const { literal } of findResourceLiterals(sourceFile, new Set(["Application"]))) {
|
|
36
|
+
const prune = getNestedBoolean(literal, ["spec", "syncPolicy", "automated", "prune"]);
|
|
37
|
+
if (prune !== true)
|
|
38
|
+
continue;
|
|
39
|
+
if (!looksProd(literal))
|
|
40
|
+
continue;
|
|
41
|
+
if (hasAnnotation(literal, ALLOW_PRUNE_ANNOTATION))
|
|
42
|
+
continue;
|
|
43
|
+
// Anchor the diagnostic at the `prune` assignment when we can find it.
|
|
44
|
+
const automated = getProp(literal, "spec");
|
|
45
|
+
const anchor = automated ?? literal;
|
|
46
|
+
const { line, column } = lineCol(sourceFile, anchor);
|
|
47
|
+
const name = getNestedString(literal, ["metadata", "name"]) ?? "(unnamed)";
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
file: sourceFile.fileName,
|
|
50
|
+
line,
|
|
51
|
+
column,
|
|
52
|
+
ruleId: "ARGO001",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: `Production Application "${name}" enables automated prune. Set syncPolicy.automated.prune=false, or opt in explicitly with the "${ALLOW_PRUNE_ANNOTATION}" annotation.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return diagnostics;
|
|
58
|
+
},
|
|
59
|
+
};
|