@intentius/chant-lexicon-k8s 0.124.31 → 0.124.32
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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the Prometheus Operator selection checks (WK8701-WK8705).
|
|
3
|
+
*
|
|
4
|
+
* An operator accepts a PrometheusRule, monitor or AlertmanagerConfig that no
|
|
5
|
+
* Prometheus or Alertmanager selects and then ignores it without an error.
|
|
6
|
+
* These helpers decide, from the manifests in one build, whether a selector
|
|
7
|
+
* picks an object, following the operator's rules for a label selector:
|
|
8
|
+
*
|
|
9
|
+
* - an object selector that is null (or absent) matches nothing, `{}` matches all;
|
|
10
|
+
* - a namespace selector that is null (or absent) matches only the selecting
|
|
11
|
+
* resource's own namespace, `{}` matches every namespace.
|
|
12
|
+
*
|
|
13
|
+
* Namespace labels come from the Namespace manifests in the build, plus the
|
|
14
|
+
* `kubernetes.io/metadata.name` label every namespace carries. A selector on a
|
|
15
|
+
* label the build cannot know (the namespace is not declared in it) answers
|
|
16
|
+
* "unknown", and the checks stay silent on "unknown".
|
|
17
|
+
*/
|
|
18
|
+
import { docsToManifests } from "./k8s-helpers.js";
|
|
19
|
+
export const MONITORING_GROUP = "monitoring.coreos.com/";
|
|
20
|
+
function isRecord(v) {
|
|
21
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
/** A manifest of a monitoring.coreos.com kind, whatever the version. */
|
|
24
|
+
export function ofMonitoringKind(m, kind) {
|
|
25
|
+
return m.kind === kind && typeof m.apiVersion === "string" && m.apiVersion.startsWith(MONITORING_GROUP);
|
|
26
|
+
}
|
|
27
|
+
export function monitoringKind(manifests, kind) {
|
|
28
|
+
return manifests.filter((m) => ofMonitoringKind(m, kind));
|
|
29
|
+
}
|
|
30
|
+
export function namespaceOf(m) {
|
|
31
|
+
return typeof m.metadata?.namespace === "string" && m.metadata.namespace !== "" ? m.metadata.namespace : "default";
|
|
32
|
+
}
|
|
33
|
+
export function nameOf(m) {
|
|
34
|
+
return typeof m.metadata?.name === "string" ? m.metadata.name : "";
|
|
35
|
+
}
|
|
36
|
+
export function describe(m) {
|
|
37
|
+
return `${m.kind} ${namespaceOf(m)}/${nameOf(m)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a label selector matches a label set. Null, undefined and anything
|
|
41
|
+
* that is not an object match nothing; `{}` matches everything.
|
|
42
|
+
* `unknownKeys` are label keys the caller cannot read (a namespace the build
|
|
43
|
+
* does not declare); a requirement on one answers "unknown".
|
|
44
|
+
*/
|
|
45
|
+
export function matchLabelSelector(selector, labels, unknownKeys = new Set()) {
|
|
46
|
+
if (!isRecord(selector))
|
|
47
|
+
return "no";
|
|
48
|
+
let unknown = false;
|
|
49
|
+
const matchLabels = isRecord(selector.matchLabels) ? selector.matchLabels : {};
|
|
50
|
+
for (const [k, v] of Object.entries(matchLabels)) {
|
|
51
|
+
if (unknownKeys.has(k)) {
|
|
52
|
+
unknown = true;
|
|
53
|
+
}
|
|
54
|
+
else if (labels[k] !== String(v)) {
|
|
55
|
+
return "no";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const exprs = Array.isArray(selector.matchExpressions) ? selector.matchExpressions : [];
|
|
59
|
+
for (const e of exprs) {
|
|
60
|
+
if (!isRecord(e) || typeof e.key !== "string")
|
|
61
|
+
continue;
|
|
62
|
+
if (unknownKeys.has(e.key)) {
|
|
63
|
+
unknown = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const values = Array.isArray(e.values) ? e.values.map(String) : [];
|
|
67
|
+
const has = Object.prototype.hasOwnProperty.call(labels, e.key);
|
|
68
|
+
switch (e.operator) {
|
|
69
|
+
case "In":
|
|
70
|
+
if (!has || !values.includes(labels[e.key]))
|
|
71
|
+
return "no";
|
|
72
|
+
break;
|
|
73
|
+
case "NotIn":
|
|
74
|
+
if (has && values.includes(labels[e.key]))
|
|
75
|
+
return "no";
|
|
76
|
+
break;
|
|
77
|
+
case "Exists":
|
|
78
|
+
if (!has)
|
|
79
|
+
return "no";
|
|
80
|
+
break;
|
|
81
|
+
case "DoesNotExist":
|
|
82
|
+
if (has)
|
|
83
|
+
return "no";
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return unknown ? "unknown" : "yes";
|
|
88
|
+
}
|
|
89
|
+
export function labelsOf(m) {
|
|
90
|
+
const out = {};
|
|
91
|
+
const labels = m.metadata?.labels;
|
|
92
|
+
if (isRecord(labels))
|
|
93
|
+
for (const [k, v] of Object.entries(labels))
|
|
94
|
+
out[k] = String(v);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** The namespaces a build declares, with their labels. */
|
|
98
|
+
function declaredNamespaces(manifests) {
|
|
99
|
+
const out = new Map();
|
|
100
|
+
for (const m of manifests) {
|
|
101
|
+
if (m.kind === "Namespace" && (m.apiVersion === "v1" || m.apiVersion === undefined) && nameOf(m)) {
|
|
102
|
+
out.set(nameOf(m), { ...labelsOf(m), "kubernetes.io/metadata.name": nameOf(m) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a selecting resource's namespace selector picks `targetNamespace`.
|
|
109
|
+
* Null or absent: the selecting resource's own namespace only.
|
|
110
|
+
*/
|
|
111
|
+
export function matchNamespaceSelector(selector, ownNamespace, targetNamespace, namespaces) {
|
|
112
|
+
if (!isRecord(selector))
|
|
113
|
+
return ownNamespace === targetNamespace ? "yes" : "no";
|
|
114
|
+
const declared = namespaces.get(targetNamespace);
|
|
115
|
+
const labels = declared ?? { "kubernetes.io/metadata.name": targetNamespace };
|
|
116
|
+
// A namespace the build does not declare has labels the build cannot know, except the name label.
|
|
117
|
+
const unknownKeys = new Set();
|
|
118
|
+
if (!declared) {
|
|
119
|
+
const keys = [
|
|
120
|
+
...Object.keys(isRecord(selector.matchLabels) ? selector.matchLabels : {}),
|
|
121
|
+
...(Array.isArray(selector.matchExpressions) ? selector.matchExpressions : []).map((e) => (isRecord(e) ? String(e.key) : "")),
|
|
122
|
+
];
|
|
123
|
+
for (const k of keys)
|
|
124
|
+
if (k !== "kubernetes.io/metadata.name")
|
|
125
|
+
unknownKeys.add(k);
|
|
126
|
+
}
|
|
127
|
+
return matchLabelSelector(selector, labels, unknownKeys);
|
|
128
|
+
}
|
|
129
|
+
/** A selecting resource's reach over one object: its object selector, then its namespace selector. */
|
|
130
|
+
export function selects(selector, namespaceSelector, selecting, object, namespaces) {
|
|
131
|
+
const ns = matchNamespaceSelector(namespaceSelector, namespaceOf(selecting), namespaceOf(object), namespaces);
|
|
132
|
+
if (ns === "no")
|
|
133
|
+
return "no";
|
|
134
|
+
const obj = matchLabelSelector(selector, labelsOf(object));
|
|
135
|
+
if (obj === "no")
|
|
136
|
+
return "no";
|
|
137
|
+
return ns === "unknown" || obj === "unknown" ? "unknown" : "yes";
|
|
138
|
+
}
|
|
139
|
+
export function selectionContext(ctx) {
|
|
140
|
+
const manifests = docsToManifests(ctx);
|
|
141
|
+
return { manifests, namespaces: declaredNamespaces(manifests) };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Objects of `kinds` that no resource in `selectors` selects, by the named
|
|
145
|
+
* object selector and namespace selector fields of each selector's spec.
|
|
146
|
+
* Silent (empty) when `selectors` is empty, since the stack is often installed
|
|
147
|
+
* separately. A selector that answers "unknown" counts as selecting.
|
|
148
|
+
*/
|
|
149
|
+
export function unselected(sc, objects, selectors, fields) {
|
|
150
|
+
if (selectors.length === 0)
|
|
151
|
+
return [];
|
|
152
|
+
return objects.filter((object) => {
|
|
153
|
+
for (const sel of selectors) {
|
|
154
|
+
const f = fields(sel, object);
|
|
155
|
+
if (!f)
|
|
156
|
+
continue;
|
|
157
|
+
const spec = (sel.spec ?? {});
|
|
158
|
+
if (selects(spec[f[0]], spec[f[1]], sel, object, sc.namespaces) !== "no")
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each OpenTelemetry Collector config in a k8s build runs: the join
|
|
3
|
+
* WK8601, WK8602 and WK8603 read (chant #2899).
|
|
4
|
+
*
|
|
5
|
+
* The collector config is a YAML string inside a ConfigMap, and whether it
|
|
6
|
+
* runs once per node or as N replicas lives on another document, the workload
|
|
7
|
+
* that mounts it. A config is joined to its workload in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. The workload's `otel.chant.dev/config` annotation, written by
|
|
10
|
+
* `OtelCollector` and `OtelCollectorGateway`, naming the ConfigMap whose
|
|
11
|
+
* `config.yaml` it runs.
|
|
12
|
+
* 2. Otherwise the ConfigMaps the pod spec mounts as volumes (how
|
|
13
|
+
* `GkeOtelCollector` and hand-written manifests link them), keeping only
|
|
14
|
+
* data keys that parse as a collector config (a `service.pipelines` map).
|
|
15
|
+
* When a container passes `--config=<path>`, only keys named by those
|
|
16
|
+
* paths count.
|
|
17
|
+
*
|
|
18
|
+
* 3. An OpenTelemetry Operator `OpenTelemetryCollector` carries its config in
|
|
19
|
+
* `spec.config` (an object in v1beta1, a YAML string in v1alpha1) and its
|
|
20
|
+
* placement in `spec.mode` and `spec.replicas`; it is its own workload. A
|
|
21
|
+
* `sidecar` runs inside other pods and is left out. The Services the
|
|
22
|
+
* operator makes (`<name>-collector`, `<name>-collector-headless`) are not
|
|
23
|
+
* in the build, so \`servicesSelecting\` names them from the CR.
|
|
24
|
+
*
|
|
25
|
+
* Anything this can't resolve (a ConfigMap or workload outside the build, a
|
|
26
|
+
* replica count that isn't a number) is left out, so the checks stay silent
|
|
27
|
+
* rather than guess. Like every bundle-join check they see one build root at
|
|
28
|
+
* a time (chant #1939).
|
|
29
|
+
*
|
|
30
|
+
* The annotation keys are repeated here rather than imported from the
|
|
31
|
+
* composites, whose module pulls in the otel lexicon; a test pins them to
|
|
32
|
+
* `OTEL_COLLECTOR_ANNOTATIONS`.
|
|
33
|
+
*/
|
|
34
|
+
import { parseCollectorConfig as parseOtelCollectorConfig } from "@intentius/chant-lexicon-otel/configmap";
|
|
35
|
+
import { extractContainers, extractPodSpec } from "./k8s-helpers.js";
|
|
36
|
+
export const PLACEMENT_ANNOTATIONS = {
|
|
37
|
+
role: "otel.chant.dev/role",
|
|
38
|
+
workload: "otel.chant.dev/workload",
|
|
39
|
+
config: "otel.chant.dev/config",
|
|
40
|
+
gateways: "otel.chant.dev/gateways",
|
|
41
|
+
header: "otel.chant.dev/header",
|
|
42
|
+
};
|
|
43
|
+
const PLACED_KINDS = new Set(["DaemonSet", "Deployment", "StatefulSet"]);
|
|
44
|
+
const OTEL_OPERATOR_KINDS = { daemonset: "DaemonSet", deployment: "Deployment", statefulset: "StatefulSet" };
|
|
45
|
+
/** An OpenTelemetry Operator `OpenTelemetryCollector` (any version of `opentelemetry.io`). */
|
|
46
|
+
export function isOperatorCollector(m) {
|
|
47
|
+
return m.kind === "OpenTelemetryCollector" && typeof m.apiVersion === "string" && m.apiVersion.startsWith("opentelemetry.io/");
|
|
48
|
+
}
|
|
49
|
+
/** The placement of one `OpenTelemetryCollector`, or undefined for a sidecar, a missing name or a config that isn't one. */
|
|
50
|
+
function operatorPlacement(cr) {
|
|
51
|
+
const name = cr.metadata?.name;
|
|
52
|
+
if (typeof name !== "string")
|
|
53
|
+
return undefined;
|
|
54
|
+
const mode = cr.spec?.mode === undefined || cr.spec.mode === null ? "deployment" : cr.spec.mode;
|
|
55
|
+
if (typeof mode !== "string" || !(mode in OTEL_OPERATOR_KINDS))
|
|
56
|
+
return undefined;
|
|
57
|
+
const kind = OTEL_OPERATOR_KINDS[mode];
|
|
58
|
+
// v1beta1 holds the config as an object, v1alpha1 as YAML text.
|
|
59
|
+
const raw = cr.spec?.config;
|
|
60
|
+
const config = isRecord(raw)
|
|
61
|
+
? parseCollectorConfig(JSON.stringify(raw))
|
|
62
|
+
: parseCollectorConfig(raw);
|
|
63
|
+
if (!config)
|
|
64
|
+
return undefined;
|
|
65
|
+
let replicas;
|
|
66
|
+
if (kind !== "DaemonSet") {
|
|
67
|
+
const r = cr.spec?.replicas;
|
|
68
|
+
const base = r === undefined || r === null ? 1 : typeof r === "number" ? r : undefined;
|
|
69
|
+
const auto = isRecord(cr.spec?.autoscaler) ? cr.spec.autoscaler.maxReplicas : undefined;
|
|
70
|
+
replicas = base === undefined ? undefined : Math.max(base, typeof auto === "number" ? auto : 0);
|
|
71
|
+
}
|
|
72
|
+
return { workload: cr, kind, name, namespace: ns(cr), replicas, configMap: name, key: "spec.config", config, where: "spec.config", operatorCR: true };
|
|
73
|
+
}
|
|
74
|
+
function ns(m) {
|
|
75
|
+
const n = m.metadata?.namespace;
|
|
76
|
+
return typeof n === "string" && n.length > 0 ? n : "default";
|
|
77
|
+
}
|
|
78
|
+
function annotations(m) {
|
|
79
|
+
const a = m.metadata?.annotations;
|
|
80
|
+
return a && typeof a === "object" ? a : {};
|
|
81
|
+
}
|
|
82
|
+
function isRecord(v) {
|
|
83
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a ConfigMap value as a collector config, or undefined when it isn't
|
|
87
|
+
* one. The otel lexicon's parser, shared with its config checks (OTEL101 and
|
|
88
|
+
* the rest) and WK8604 so every check agrees on what counts as a config.
|
|
89
|
+
*/
|
|
90
|
+
export function parseCollectorConfig(text) {
|
|
91
|
+
return parseOtelCollectorConfig(text);
|
|
92
|
+
}
|
|
93
|
+
/** The `type` of a component id: `tail_sampling/errors` is `tail_sampling`. */
|
|
94
|
+
export function componentType(id) {
|
|
95
|
+
const slash = id.indexOf("/");
|
|
96
|
+
return slash === -1 ? id : id.slice(0, slash);
|
|
97
|
+
}
|
|
98
|
+
function stringList(v) {
|
|
99
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
100
|
+
}
|
|
101
|
+
/** The pipelines of one signal (`traces`, `traces/sampled`, ...) as [id, pipeline] pairs. */
|
|
102
|
+
export function pipelinesOf(config, signal) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const [id, p] of Object.entries(config.service.pipelines)) {
|
|
105
|
+
if (componentType(id) !== signal || !isRecord(p))
|
|
106
|
+
continue;
|
|
107
|
+
out.push([id, { receivers: stringList(p.receivers), processors: stringList(p.processors), exporters: stringList(p.exporters) }]);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/** Every pipeline, whatever its signal. */
|
|
112
|
+
export function allPipelines(config) {
|
|
113
|
+
return ["traces", "metrics", "logs"].flatMap((s) => pipelinesOf(config, s));
|
|
114
|
+
}
|
|
115
|
+
/** Processor ids of `type` that a traces pipeline runs. */
|
|
116
|
+
export function tracePipelineProcessors(config, type) {
|
|
117
|
+
const out = new Set();
|
|
118
|
+
for (const [, p] of pipelinesOf(config, "traces")) {
|
|
119
|
+
for (const id of p.processors)
|
|
120
|
+
if (componentType(id) === type)
|
|
121
|
+
out.add(id);
|
|
122
|
+
}
|
|
123
|
+
return [...out];
|
|
124
|
+
}
|
|
125
|
+
/** The ConfigMap file names a workload's containers pass with `--config`. */
|
|
126
|
+
function configArgNames(workload) {
|
|
127
|
+
const names = [];
|
|
128
|
+
for (const c of extractContainers(workload)) {
|
|
129
|
+
const argv = [...stringList(c.command), ...stringList(c.args)];
|
|
130
|
+
for (let i = 0; i < argv.length; i++) {
|
|
131
|
+
let path;
|
|
132
|
+
if (argv[i].startsWith("--config="))
|
|
133
|
+
path = argv[i].slice("--config=".length);
|
|
134
|
+
else if (argv[i] === "--config")
|
|
135
|
+
path = argv[i + 1];
|
|
136
|
+
if (!path)
|
|
137
|
+
continue;
|
|
138
|
+
// `--config=env:...`, `--config=yaml:...` and URLs are not files.
|
|
139
|
+
if (/^[a-z]+:/.test(path))
|
|
140
|
+
continue;
|
|
141
|
+
names.push(path.slice(path.lastIndexOf("/") + 1));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return names;
|
|
145
|
+
}
|
|
146
|
+
/** ConfigMap names mounted by a workload's pod spec, directly or through a projected volume. */
|
|
147
|
+
function mountedConfigMaps(workload) {
|
|
148
|
+
const pod = extractPodSpec(workload);
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const v of Array.isArray(pod?.volumes) ? pod.volumes : []) {
|
|
151
|
+
if (!isRecord(v))
|
|
152
|
+
continue;
|
|
153
|
+
const cm = isRecord(v.configMap) ? v.configMap.name : undefined;
|
|
154
|
+
if (typeof cm === "string")
|
|
155
|
+
out.push(cm);
|
|
156
|
+
const sources = isRecord(v.projected) && Array.isArray(v.projected.sources) ? v.projected.sources : [];
|
|
157
|
+
for (const s of sources) {
|
|
158
|
+
const name = isRecord(s) && isRecord(s.configMap) ? s.configMap.name : undefined;
|
|
159
|
+
if (typeof name === "string")
|
|
160
|
+
out.push(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
/** The max replica count an HPA allows for kind/name in a namespace, if one targets it. */
|
|
166
|
+
function hpaMax(manifests, kind, name, namespace) {
|
|
167
|
+
let max;
|
|
168
|
+
for (const m of manifests) {
|
|
169
|
+
if (m.kind !== "HorizontalPodAutoscaler" || ns(m) !== namespace)
|
|
170
|
+
continue;
|
|
171
|
+
const target = isRecord(m.spec?.scaleTargetRef) ? m.spec.scaleTargetRef : undefined;
|
|
172
|
+
if (!target || target.kind !== kind || target.name !== name)
|
|
173
|
+
continue;
|
|
174
|
+
const n = m.spec?.maxReplicas;
|
|
175
|
+
if (typeof n === "number")
|
|
176
|
+
max = Math.max(max ?? 0, n);
|
|
177
|
+
}
|
|
178
|
+
return max;
|
|
179
|
+
}
|
|
180
|
+
/** Join every collector config in the build to the workload that runs it. */
|
|
181
|
+
export function collectorPlacements(manifests) {
|
|
182
|
+
const configMaps = new Map();
|
|
183
|
+
for (const m of manifests) {
|
|
184
|
+
if (m.kind === "ConfigMap" && typeof m.metadata?.name === "string")
|
|
185
|
+
configMaps.set(`${ns(m)}/${m.metadata.name}`, m);
|
|
186
|
+
}
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const workload of manifests) {
|
|
189
|
+
if (!workload.kind || !PLACED_KINDS.has(workload.kind))
|
|
190
|
+
continue;
|
|
191
|
+
const name = workload.metadata?.name;
|
|
192
|
+
if (typeof name !== "string")
|
|
193
|
+
continue;
|
|
194
|
+
const namespace = ns(workload);
|
|
195
|
+
const kind = workload.kind;
|
|
196
|
+
let replicas;
|
|
197
|
+
if (kind !== "DaemonSet") {
|
|
198
|
+
const r = workload.spec?.replicas;
|
|
199
|
+
const base = r === undefined || r === null ? 1 : typeof r === "number" ? r : undefined;
|
|
200
|
+
const hpa = hpaMax(manifests, kind, name, namespace);
|
|
201
|
+
replicas = base === undefined ? undefined : Math.max(base, hpa ?? 0);
|
|
202
|
+
}
|
|
203
|
+
const annotated = annotations(workload)[PLACEMENT_ANNOTATIONS.config];
|
|
204
|
+
const candidates = typeof annotated === "string"
|
|
205
|
+
? [{ configMap: annotated, keys: ["config.yaml"] }]
|
|
206
|
+
: mountedConfigMaps(workload).map((configMap) => ({ configMap }));
|
|
207
|
+
const argNames = configArgNames(workload);
|
|
208
|
+
for (const { configMap, keys } of candidates) {
|
|
209
|
+
const cm = configMaps.get(`${namespace}/${configMap}`);
|
|
210
|
+
if (!cm || !isRecord(cm.data))
|
|
211
|
+
continue;
|
|
212
|
+
const parsed = [];
|
|
213
|
+
for (const [key, text] of Object.entries(cm.data)) {
|
|
214
|
+
if (keys && !keys.includes(key))
|
|
215
|
+
continue;
|
|
216
|
+
const config = parseCollectorConfig(text);
|
|
217
|
+
if (config)
|
|
218
|
+
parsed.push([key, config]);
|
|
219
|
+
}
|
|
220
|
+
const named = parsed.filter(([key]) => argNames.includes(key));
|
|
221
|
+
for (const [key, config] of named.length > 0 ? named : parsed) {
|
|
222
|
+
out.push({ workload, kind, name, namespace, replicas, configMap, key, config, where: `ConfigMap ${configMap}, ${key}` });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const m of manifests) {
|
|
227
|
+
if (!isOperatorCollector(m))
|
|
228
|
+
continue;
|
|
229
|
+
const p = operatorPlacement(m);
|
|
230
|
+
if (p)
|
|
231
|
+
out.push(p);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
/** How a placement is described in a message: `DaemonSet observability/otel-agent`. */
|
|
236
|
+
export function describePlacement(p) {
|
|
237
|
+
if (p.operatorCR)
|
|
238
|
+
return `OpenTelemetryCollector ${p.namespace}/${p.name} (mode ${p.kind.toLowerCase()})`;
|
|
239
|
+
return `${p.kind} ${p.namespace}/${p.name}`;
|
|
240
|
+
}
|
|
241
|
+
/** The Services in a workload's namespace whose selector matches its pod template labels. */
|
|
242
|
+
export function servicesSelecting(manifests, workload) {
|
|
243
|
+
// The operator's Services for a CR are not in the build; their names are fixed by the operator.
|
|
244
|
+
if (isOperatorCollector(workload) && typeof workload.metadata?.name === "string") {
|
|
245
|
+
const name = workload.metadata.name;
|
|
246
|
+
return [
|
|
247
|
+
{ name: `${name}-collector`, namespace: ns(workload), headless: false },
|
|
248
|
+
{ name: `${name}-collector-headless`, namespace: ns(workload), headless: true },
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
const template = isRecord(workload.spec?.template) ? workload.spec.template : undefined;
|
|
252
|
+
const labels = isRecord(template?.metadata) && isRecord(template.metadata.labels) ? template.metadata.labels : {};
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const m of manifests) {
|
|
255
|
+
if (m.kind !== "Service" || ns(m) !== ns(workload) || typeof m.metadata?.name !== "string")
|
|
256
|
+
continue;
|
|
257
|
+
const selector = isRecord(m.spec?.selector) ? m.spec.selector : undefined;
|
|
258
|
+
if (!selector || Object.keys(selector).length === 0)
|
|
259
|
+
continue;
|
|
260
|
+
if (!Object.entries(selector).every(([k, v]) => labels[k] === v))
|
|
261
|
+
continue;
|
|
262
|
+
out.push({ name: m.metadata.name, namespace: ns(m), headless: m.spec?.clusterIP === "None" });
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The Service a hostname names, as `name.namespace`, when it has the shape of
|
|
268
|
+
* cluster DNS: `name`, `name.ns`, `name.ns.svc` or `name.ns.svc.<domain>`.
|
|
269
|
+
* A bare `name` resolves in the caller's namespace.
|
|
270
|
+
*/
|
|
271
|
+
export function serviceOfHost(host, callerNamespace) {
|
|
272
|
+
const parts = host.split(".");
|
|
273
|
+
if (parts.some((p) => p.length === 0))
|
|
274
|
+
return undefined;
|
|
275
|
+
if (parts.length === 1)
|
|
276
|
+
return `${parts[0]}.${callerNamespace}`;
|
|
277
|
+
if (parts.length === 2)
|
|
278
|
+
return `${parts[0]}.${parts[1]}`;
|
|
279
|
+
if (parts[2] === "svc")
|
|
280
|
+
return `${parts[0]}.${parts[1]}`;
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
/** The host of an exporter endpoint: `host:port`, `scheme://host:port/path` or `dns:///host:port`. */
|
|
284
|
+
export function hostOfEndpoint(endpoint) {
|
|
285
|
+
if (typeof endpoint !== "string" || endpoint.length === 0)
|
|
286
|
+
return undefined;
|
|
287
|
+
let rest = endpoint.replace(/^[a-z][a-z0-9+.-]*:\/\/\/?/i, "");
|
|
288
|
+
rest = rest.split("/")[0];
|
|
289
|
+
if (rest.startsWith("["))
|
|
290
|
+
return undefined;
|
|
291
|
+
const colon = rest.lastIndexOf(":");
|
|
292
|
+
return colon === -1 ? rest : rest.slice(0, colon);
|
|
293
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getAdditionalFiles } from "./k8s-helpers.js";
|
|
2
|
+
import { committedEncryptedDeclarations, sidecarFilename, validateEncryptedSecretDocument } from "../../sops/encrypted-secret-file.js";
|
|
3
|
+
/** Every sidecar file across every lexicon output, keyed by filename. */
|
|
4
|
+
export function allAdditionalFiles(ctx) {
|
|
5
|
+
const files = new Map();
|
|
6
|
+
for (const [, output] of ctx.outputs) {
|
|
7
|
+
for (const [filename, content] of Object.entries(getAdditionalFiles(output))) {
|
|
8
|
+
files.set(filename, content);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return files;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Join every committed-encrypted declaration to its emitted sidecar,
|
|
15
|
+
* validating the document as it goes. A declaration with no sidecar comes
|
|
16
|
+
* back with `text` undefined and one problem naming the missing file.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveEncryptedSecretClaims(ctx) {
|
|
19
|
+
const declarations = committedEncryptedDeclarations(ctx.entities);
|
|
20
|
+
if (declarations.size === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const files = allAdditionalFiles(ctx);
|
|
23
|
+
const resolved = [];
|
|
24
|
+
for (const [entityName, declaration] of declarations) {
|
|
25
|
+
const filename = sidecarFilename(declaration.file);
|
|
26
|
+
const text = files.get(filename);
|
|
27
|
+
if (text === undefined) {
|
|
28
|
+
resolved.push({
|
|
29
|
+
entityName,
|
|
30
|
+
declaration,
|
|
31
|
+
filename,
|
|
32
|
+
problems: [
|
|
33
|
+
`the build emitted no file for "${declaration.file}" — ` +
|
|
34
|
+
`the declared ciphertext did not resolve`,
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const { problems, document } = validateEncryptedSecretDocument(text, declaration);
|
|
40
|
+
resolved.push({ entityName, declaration, filename, text, problems, document });
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8005: Hardcoded Secrets in Environment Variables
|
|
3
|
+
*
|
|
4
|
+
* Detects container environment variables with names suggesting sensitive
|
|
5
|
+
* values (password, token, key, secret) that use hardcoded string values
|
|
6
|
+
* instead of secretKeyRef or configMapKeyRef.
|
|
7
|
+
*/
|
|
8
|
+
import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers.js";
|
|
9
|
+
const SENSITIVE_NAME_PATTERN = /password|token|key|secret/i;
|
|
10
|
+
export const wk8005 = {
|
|
11
|
+
id: "WK8005",
|
|
12
|
+
description: "Hardcoded secrets in env vars — sensitive environment variables should use secretKeyRef",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
16
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind))
|
|
17
|
+
continue;
|
|
18
|
+
const containers = extractContainers(manifest);
|
|
19
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
20
|
+
for (const container of containers) {
|
|
21
|
+
if (!Array.isArray(container.env))
|
|
22
|
+
continue;
|
|
23
|
+
for (const envVar of container.env) {
|
|
24
|
+
if (typeof envVar.name === "string" &&
|
|
25
|
+
SENSITIVE_NAME_PATTERN.test(envVar.name) &&
|
|
26
|
+
envVar.value !== undefined &&
|
|
27
|
+
envVar.value !== null &&
|
|
28
|
+
typeof envVar.value === "string" &&
|
|
29
|
+
envVar.value !== "" &&
|
|
30
|
+
!envVar.valueFrom) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "WK8005",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has hardcoded value for sensitive env var "${envVar.name}" — use secretKeyRef instead`,
|
|
35
|
+
entity: resourceName,
|
|
36
|
+
lexicon: "k8s",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return diagnostics;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8006: No :latest or Untagged Images
|
|
3
|
+
*
|
|
4
|
+
* Container images should use explicit version tags for reproducibility.
|
|
5
|
+
* Flags images using the `:latest` tag or no tag at all.
|
|
6
|
+
*/
|
|
7
|
+
import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers.js";
|
|
8
|
+
export const wk8006 = {
|
|
9
|
+
id: "WK8006",
|
|
10
|
+
description: "No :latest or untagged images — container images should use explicit version tags",
|
|
11
|
+
check(ctx) {
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
14
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind))
|
|
15
|
+
continue;
|
|
16
|
+
const containers = extractContainers(manifest);
|
|
17
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
18
|
+
for (const container of containers) {
|
|
19
|
+
const image = container.image;
|
|
20
|
+
if (typeof image !== "string" || image === "")
|
|
21
|
+
continue;
|
|
22
|
+
// Split off digest (@sha256:...) — those are pinned and fine
|
|
23
|
+
if (image.includes("@"))
|
|
24
|
+
continue;
|
|
25
|
+
// Extract the tag portion (after the last colon that isn't part of a port)
|
|
26
|
+
// Images can be: name, name:tag, registry:port/name, registry:port/name:tag
|
|
27
|
+
const slashIndex = image.lastIndexOf("/");
|
|
28
|
+
const afterSlash = slashIndex >= 0 ? image.slice(slashIndex + 1) : image;
|
|
29
|
+
const colonIndex = afterSlash.lastIndexOf(":");
|
|
30
|
+
if (colonIndex === -1) {
|
|
31
|
+
// No tag at all
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WK8006",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses untagged image "${image}" — specify an explicit version tag`,
|
|
36
|
+
entity: resourceName,
|
|
37
|
+
lexicon: "k8s",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const tag = afterSlash.slice(colonIndex + 1);
|
|
42
|
+
if (tag === "latest") {
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "WK8006",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses :latest tag on image "${image}" — use a specific version tag`,
|
|
47
|
+
entity: resourceName,
|
|
48
|
+
lexicon: "k8s",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return diagnostics;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8041: Hardcoded API Keys
|
|
3
|
+
*
|
|
4
|
+
* Detects well-known API key patterns in container environment variable values.
|
|
5
|
+
* Catches Stripe keys, GitHub PATs, AWS access keys, Google API keys, and
|
|
6
|
+
* other common credential formats.
|
|
7
|
+
*/
|
|
8
|
+
import { docsToManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers.js";
|
|
9
|
+
/**
|
|
10
|
+
* Known API key patterns and their descriptions.
|
|
11
|
+
*/
|
|
12
|
+
const API_KEY_PATTERNS = [
|
|
13
|
+
{ pattern: /^sk_live_/, label: "Stripe secret key" },
|
|
14
|
+
{ pattern: /^pk_live_/, label: "Stripe publishable key" },
|
|
15
|
+
{ pattern: /^sk_test_/, label: "Stripe test secret key" },
|
|
16
|
+
{ pattern: /^pk_test_/, label: "Stripe test publishable key" },
|
|
17
|
+
{ pattern: /^ghp_[A-Za-z0-9_]{36,}/, label: "GitHub personal access token" },
|
|
18
|
+
{ pattern: /^gho_[A-Za-z0-9_]{36,}/, label: "GitHub OAuth token" },
|
|
19
|
+
{ pattern: /^ghs_[A-Za-z0-9_]{36,}/, label: "GitHub app installation token" },
|
|
20
|
+
{ pattern: /^ghu_[A-Za-z0-9_]{36,}/, label: "GitHub user-to-server token" },
|
|
21
|
+
{ pattern: /^ghr_[A-Za-z0-9_]{36,}/, label: "GitHub refresh token" },
|
|
22
|
+
{ pattern: /^AKIA[A-Z0-9]{16}$/, label: "AWS access key ID" },
|
|
23
|
+
{ pattern: /^AIza[A-Za-z0-9_-]{35}$/, label: "Google API key" },
|
|
24
|
+
{ pattern: /^xox[bprs]-[A-Za-z0-9-]+/, label: "Slack token" },
|
|
25
|
+
{ pattern: /^SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}$/, label: "SendGrid API key" },
|
|
26
|
+
];
|
|
27
|
+
export const wk8041 = {
|
|
28
|
+
id: "WK8041",
|
|
29
|
+
description: "Hardcoded API keys — detects well-known API key patterns in env var values",
|
|
30
|
+
check(ctx) {
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
for (const manifest of docsToManifests(ctx)) {
|
|
33
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind))
|
|
34
|
+
continue;
|
|
35
|
+
const containers = extractContainers(manifest);
|
|
36
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
37
|
+
for (const container of containers) {
|
|
38
|
+
if (!Array.isArray(container.env))
|
|
39
|
+
continue;
|
|
40
|
+
for (const envVar of container.env) {
|
|
41
|
+
if (typeof envVar.value !== "string" || envVar.value === "")
|
|
42
|
+
continue;
|
|
43
|
+
for (const { pattern, label } of API_KEY_PATTERNS) {
|
|
44
|
+
if (pattern.test(envVar.value)) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
checkId: "WK8041",
|
|
47
|
+
severity: "error",
|
|
48
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has a ${label} hardcoded in env var "${envVar.name ?? "(unnamed)"}" — use a Secret reference instead`,
|
|
49
|
+
entity: resourceName,
|
|
50
|
+
lexicon: "k8s",
|
|
51
|
+
});
|
|
52
|
+
break; // One match per env var is sufficient
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return diagnostics;
|
|
59
|
+
},
|
|
60
|
+
};
|