@intentius/chant-lexicon-k8s 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/sweep-types.d.ts +1 -2
- package/dist/api/sweep-types.d.ts.map +1 -1
- package/dist/composites/operator-stack.d.ts +19 -18
- package/dist/composites/operator-stack.d.ts.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/deep-observe-hooks.d.ts +4 -4
- package/dist/deep-observe.d.ts +23 -16
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/export-resources.d.ts +1 -1
- package/dist/integrity.json +3 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +10 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/argo.d.ts +4 -4
- package/dist/op/activities/argo.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +9 -7
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +7 -7
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +6 -5
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/op/builders.d.ts +13 -13
- package/dist/skills/chant-k8s-argo.md +27 -13
- package/package.json +3 -3
- package/src/api/sweep-types.ts +1 -2
- package/src/composites/composites.test.ts +13 -2
- package/src/composites/operator-stack.ts +22 -20
- package/src/config.ts +4 -4
- package/src/crd/crd-sources.ts +4 -4
- package/src/deep-observe-hooks.ts +4 -4
- package/src/deep-observe.test.ts +219 -46
- package/src/deep-observe.ts +28 -21
- package/src/export-resources.ts +1 -1
- package/src/lint/audit-catalog.ts +5 -1
- package/src/lint/audit-lineage.ts +127 -0
- package/src/op/activities/argo.test.ts +7 -8
- package/src/op/activities/argo.ts +4 -4
- package/src/op/activities/index.ts +9 -7
- package/src/op/activities/kubectl.test.ts +5 -4
- package/src/op/activities/kubectl.ts +52 -67
- package/src/op/activities/wait-for-ready.test.ts +2 -2
- package/src/op/activities/wait-for-ready.ts +7 -7
- package/src/op/builders.ts +13 -13
- package/src/plugin.ts +1 -1
- package/src/serializer.test.ts +26 -0
- package/src/skills/chant-k8s-argo.md +27 -13
package/src/deep-observe.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* ## What managedFields decides, and what it does not
|
|
11
11
|
*
|
|
12
|
-
* AWS
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The AWS and Azure rows both prune by a **static, entityType-keyed** table
|
|
13
|
+
* (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`): an ARN always looks like
|
|
14
|
+
* an ARN, `provisioningState` is always server-populated. Neither needs the
|
|
15
|
+
* specific live object in hand — it is exactly
|
|
16
16
|
* what `./deep-observe-hooks.ts`'s `k8sDeepNormalizationHooks` is, and it
|
|
17
17
|
* covers Kubernetes' *equivalent* static noise (`status`,
|
|
18
18
|
* `metadata.{uid,resourceVersion,generation,creationTimestamp}`, a handful of
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* was controller noise. It silenced exactly the case the deep read exists
|
|
32
32
|
* for — `kubectl label deploy web team=platform` is foreign-owned
|
|
33
33
|
* (`kubectl-edit`) and undeclared, and never appeared in fieldDrift. The
|
|
34
|
-
* policy now (recorded on #1202) is:
|
|
34
|
+
* policy now (recorded on #1202, refined by #2160) is:
|
|
35
35
|
*
|
|
36
36
|
* 1. **Chant-owned** (`chant`, `chant:<stack>` — chant #1075, matched on the
|
|
37
37
|
* family so a stack rename does not stop recognizing its own history) is
|
|
@@ -41,18 +41,25 @@
|
|
|
41
41
|
* write currently holds the field, and a foreign write that overrides a
|
|
42
42
|
* value chant's manifest asks for is the thing `lifecycle diff --live`
|
|
43
43
|
* exists to surface. It reports as `changed`.
|
|
44
|
-
* 3. **
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* 3. **Undeclared** — a path outside the declaration's claimed-field set
|
|
45
|
+
* (`@intentius/chant/claimed-fields`) — is reported as unclaimed and
|
|
46
|
+
* is not drift at all. Before #2160 it was `undeclared` drift that the
|
|
47
|
+
* accepted baseline had to absorb one path at a time; the declaration
|
|
48
|
+
* answers on the first read instead, so an HPA that owns `spec.replicas`
|
|
49
|
+
* on a Deployment whose source is silent about replicas is quiet with
|
|
50
|
+
* nothing recorded anywhere.
|
|
49
51
|
*
|
|
50
|
-
* So ownership decides nothing about *whether* a path is compared
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* So ownership decides nothing about *whether* a path is compared, and since
|
|
53
|
+
* #2160 it no longer decides the verdict either — the declaration does. What
|
|
54
|
+
* it contributes is `fieldOwners` (#1189), and that is worth more here than
|
|
55
|
+
* anywhere else: on every other substrate a held field can only be reported
|
|
56
|
+
* as "not chant's", while here it is reported as `hpa-controller`'s or
|
|
57
|
+
* `kubectl-edit`'s by name, which is the difference between a controller
|
|
58
|
+
* doing its job and somebody bypassing the pipeline. Core takes the manager
|
|
59
|
+
* where this reader supplies one and falls back to the claim where it does
|
|
60
|
+
* not (a path inside a key-addressed list, which no `fieldsV1` entry names in
|
|
61
|
+
* that form). The only fields subtracted because a foreign manager wrote them
|
|
62
|
+
* are the well-known system ones on the static allowlist.
|
|
56
63
|
*
|
|
57
64
|
* ## Resolving a managedFields entry against a live array
|
|
58
65
|
*
|
|
@@ -114,7 +121,7 @@ import { k8sDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
|
114
121
|
|
|
115
122
|
// Re-exported so a dynamic importer of this module (plugin.ts's
|
|
116
123
|
// `observeResourcesDeep`, a test) can get the reader and its hooks from one
|
|
117
|
-
// place, the same shape AWS
|
|
124
|
+
// place, the same shape the AWS and Azure deep-observe.ts modules offer.
|
|
118
125
|
// `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
|
|
119
126
|
// separately, directly from `./deep-observe-hooks` — that file has no
|
|
120
127
|
// dependency on `@intentius/chant-k8s-client`, so it is safe to import
|
|
@@ -244,10 +251,10 @@ export async function observeResourcesDeepK8s(
|
|
|
244
251
|
hooks: k8sDeepNormalizationHooks,
|
|
245
252
|
}),
|
|
246
253
|
// Who owns each path (#1189). Nothing is pruned by ownership any more
|
|
247
|
-
// (#1191), so a foreign-owned undeclared field reaches the diff
|
|
248
|
-
//
|
|
249
|
-
// an operator needs to tell `hpa-controller` doing its
|
|
250
|
-
// somebody running `kubectl edit`.
|
|
254
|
+
// (#1191), so a foreign-owned undeclared field still reaches the diff
|
|
255
|
+
// — as a held field since #2160 — and naming the manager is the whole
|
|
256
|
+
// question there: an operator needs to tell `hpa-controller` doing its
|
|
257
|
+
// job from somebody running `kubectl edit`.
|
|
251
258
|
...(sets.owners.size > 0 ? { fieldOwners: Object.fromEntries(sets.owners) } : {}),
|
|
252
259
|
};
|
|
253
260
|
} catch (err) {
|
package/src/export-resources.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { DEFAULT_IMPORT_TYPES } from "./api/sweep-types";
|
|
|
28
28
|
* reachable by naming it with `--selector type=...`. Defined in
|
|
29
29
|
* `./api/sweep-types.ts` and re-exported here, its original home — chant
|
|
30
30
|
* #1075's ownership-scoped prune needs the same list without pulling the
|
|
31
|
-
* import parser
|
|
31
|
+
* import parser in behind the apply activity.
|
|
32
32
|
*/
|
|
33
33
|
export { DEFAULT_IMPORT_TYPES };
|
|
34
34
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The k8s lexicon's chant audit catalog — metadata for its post-synth rules
|
|
3
3
|
* (WK8 and ARGO Kubernetes-manifest rules). Contributed via k8sPlugin.auditCatalog() (#687).
|
|
4
4
|
*/
|
|
5
|
-
import { auditRule, K8S_PSS, K8S_SECRETS, SCORECARD_PINNED, type RuleMeta } from "@intentius/chant/audit/catalog";
|
|
5
|
+
import { auditRule, K8S_PSS, K8S_SECRETS, SCORECARD_PINNED, type RuleMeta, applyLineage } from "@intentius/chant/audit/catalog";
|
|
6
|
+
import { k8sAuditLineage } from "./audit-lineage";
|
|
6
7
|
|
|
7
8
|
export const k8sAuditCatalog: Record<string, RuleMeta> = {
|
|
8
9
|
ARGO002: auditRule("ARGO002", "merge-worthy", "guidance", "Argo Application references an undeclared AppProject", "Declare the named AppProject or reference an existing project.", { category: "correctness" }),
|
|
@@ -46,3 +47,6 @@ export const k8sAuditCatalog: Record<string, RuleMeta> = {
|
|
|
46
47
|
WK8504: auditRule("WK8504", "merge-worthy", "guidance", "committed-encrypted secret declaration does not resolve", "Point `file` at the committed ciphertext, make its metadata.name match the declaration, and re-run `sops -e -i` so every data/stringData value is ENC[...].", { authority: [K8S_SECRETS] }),
|
|
47
48
|
WK8505: auditRule("WK8505", "report-only", "guidance", "committed-encrypted secret with no Flux decryption wiring", "Add decryption: \"sops\" to the FluxAppFor reconciling the path that carries the ciphertext sidecar, or ignore if that Kustomization is declared in a different build.", { category: "correctness" }),
|
|
48
49
|
};
|
|
50
|
+
|
|
51
|
+
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
52
|
+
applyLineage(k8sAuditCatalog, k8sAuditLineage);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prior art for these audit rules: the open-source tools whose checks cover the
|
|
3
|
+
* same condition, credited per rule. See packages/core/src/audit/prior-art.ts for
|
|
4
|
+
* the registry, the relation vocabulary, and why this is credit rather than
|
|
5
|
+
* authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
|
|
6
|
+
* when a credited tool's index no longer lists a rule cited here.
|
|
7
|
+
*/
|
|
8
|
+
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
9
|
+
|
|
10
|
+
export const k8sAuditLineage: Record<string, Lineage[]> = {
|
|
11
|
+
FLUX002: [
|
|
12
|
+
{ tool: "flux-docs", rule: ".spec.sourceRef", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#source-reference", relation: "overlaps" },
|
|
13
|
+
],
|
|
14
|
+
FLUX003: [
|
|
15
|
+
{ tool: "flux-docs", rule: ".spec.dependsOn", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#dependencies", relation: "overlaps" },
|
|
16
|
+
],
|
|
17
|
+
WK8005: [
|
|
18
|
+
{ tool: "polaris", rule: "sensitiveContainerEnvVar", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
|
|
19
|
+
{ tool: "kube-linter", rule: "env-var-secret", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-var-secret", relation: "overlaps" },
|
|
20
|
+
],
|
|
21
|
+
WK8006: [
|
|
22
|
+
{ tool: "kube-linter", rule: "latest-tag", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#latest-tag", relation: "equivalent" },
|
|
23
|
+
{ tool: "polaris", rule: "tagNotSpecified", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "equivalent" },
|
|
24
|
+
{ tool: "checkov", rule: "CKV_K8S_14", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
25
|
+
],
|
|
26
|
+
WK8041: [
|
|
27
|
+
{ tool: "polaris", rule: "sensitiveContainerEnvVar", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
|
|
28
|
+
{ tool: "kube-linter", rule: "env-var-secret", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-var-secret", relation: "overlaps" },
|
|
29
|
+
{ tool: "datree", rule: "prevent-exposed-secrets-aws", url: "https://hub.datree.io/built-in-rules/prevent-exposed-secrets-aws", relation: "overlaps" },
|
|
30
|
+
],
|
|
31
|
+
WK8042: [
|
|
32
|
+
{ tool: "polaris", rule: "sensitiveConfigmapContent", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
|
|
33
|
+
{ tool: "datree", rule: "prevent-exposed-secrets-privatekey", url: "https://hub.datree.io/built-in-rules/prevent-exposed-secrets-privatekey", relation: "overlaps" },
|
|
34
|
+
],
|
|
35
|
+
WK8101: [
|
|
36
|
+
{ tool: "kube-linter", rule: "mismatching-selector", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#mismatching-selector", relation: "equivalent" },
|
|
37
|
+
{ tool: "kube-score", rule: "deployment-pod-selector-labels-match-template-metadata-labels", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "equivalent" },
|
|
38
|
+
],
|
|
39
|
+
WK8105: [
|
|
40
|
+
{ tool: "polaris", rule: "pullPolicyNotAlways", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
|
|
41
|
+
{ tool: "kube-score", rule: "container-image-pull-policy", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
42
|
+
{ tool: "checkov", rule: "CKV_K8S_15", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
|
|
43
|
+
],
|
|
44
|
+
WK8201: [
|
|
45
|
+
{ tool: "polaris", rule: "cpuLimitsMissing", url: "https://polaris.docs.fairwinds.com/checks/efficiency/", relation: "overlaps" },
|
|
46
|
+
{ tool: "checkov", rule: "CKV_K8S_13", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
|
|
47
|
+
{ tool: "kube-score", rule: "container-resources", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
48
|
+
],
|
|
49
|
+
WK8202: [
|
|
50
|
+
{ tool: "kube-linter", rule: "privileged-container", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#privileged-container", relation: "equivalent" },
|
|
51
|
+
{ tool: "polaris", rule: "runAsPrivileged", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
52
|
+
{ tool: "checkov", rule: "CKV_K8S_16", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
53
|
+
],
|
|
54
|
+
WK8203: [
|
|
55
|
+
{ tool: "kube-linter", rule: "no-read-only-root-fs", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#no-read-only-root-fs", relation: "equivalent" },
|
|
56
|
+
{ tool: "polaris", rule: "notReadOnlyRootFilesystem", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
57
|
+
{ tool: "checkov", rule: "CKV_K8S_22", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
58
|
+
],
|
|
59
|
+
WK8204: [
|
|
60
|
+
{ tool: "polaris", rule: "runAsRootAllowed", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
61
|
+
{ tool: "checkov", rule: "CKV_K8S_23", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
62
|
+
{ tool: "kube-linter", rule: "run-as-non-root", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#run-as-non-root", relation: "overlaps" },
|
|
63
|
+
],
|
|
64
|
+
WK8205: [
|
|
65
|
+
{ tool: "kubesec", rule: "containers[] .securityContext .capabilities .drop | index(\"ALL\")", url: "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", relation: "equivalent" },
|
|
66
|
+
{ tool: "kics", rule: "No Drop Capabilities for Containers", url: "https://docs.kics.io/latest/queries/kubernetes-queries/268ca686-7fb7-4ae9-b129-955a2a89064e/", relation: "overlaps" },
|
|
67
|
+
{ tool: "kube-linter", rule: "drop-net-raw-capability", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#drop-net-raw-capability", relation: "overlaps" },
|
|
68
|
+
],
|
|
69
|
+
WK8207: [
|
|
70
|
+
{ tool: "kube-linter", rule: "host-network", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-network", relation: "equivalent" },
|
|
71
|
+
{ tool: "polaris", rule: "hostNetworkSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
72
|
+
{ tool: "checkov", rule: "CKV_K8S_19", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
73
|
+
],
|
|
74
|
+
WK8208: [
|
|
75
|
+
{ tool: "kube-linter", rule: "host-pid", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-pid", relation: "equivalent" },
|
|
76
|
+
{ tool: "polaris", rule: "hostPIDSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
77
|
+
{ tool: "checkov", rule: "CKV_K8S_17", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
78
|
+
],
|
|
79
|
+
WK8209: [
|
|
80
|
+
{ tool: "kube-linter", rule: "host-ipc", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-ipc", relation: "equivalent" },
|
|
81
|
+
{ tool: "polaris", rule: "hostIPCSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
|
|
82
|
+
{ tool: "checkov", rule: "CKV_K8S_18", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
|
|
83
|
+
],
|
|
84
|
+
WK8301: [
|
|
85
|
+
{ tool: "kube-score", rule: "pod-probes", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
86
|
+
{ tool: "polaris", rule: "readinessProbeMissing", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
|
|
87
|
+
{ tool: "checkov", rule: "CKV_K8S_8", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
|
|
88
|
+
],
|
|
89
|
+
WK8302: [
|
|
90
|
+
{ tool: "polaris", rule: "deploymentMissingReplicas", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "equivalent" },
|
|
91
|
+
{ tool: "kube-score", rule: "deployment-replicas", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "equivalent" },
|
|
92
|
+
{ tool: "datree", rule: "ensure-minimum-two-replicas", url: "https://hub.datree.io/built-in-rules/ensure-minimum-two-replicas", relation: "equivalent" },
|
|
93
|
+
],
|
|
94
|
+
WK8303: [
|
|
95
|
+
{ tool: "kube-score", rule: "deployment-has-poddisruptionbudget", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
96
|
+
{ tool: "polaris", rule: "missingPodDisruptionBudget", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
|
|
97
|
+
{ tool: "kics", rule: "Deployment Without PodDisruptionBudget", url: "https://docs.kics.io/latest/queries/kubernetes-queries/b23e9b98-0cb6-4fc9-b257-1f3270442678/", relation: "overlaps" },
|
|
98
|
+
],
|
|
99
|
+
WK8304: [
|
|
100
|
+
{ tool: "polaris", rule: "tlsSettingsMissing", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
|
|
101
|
+
],
|
|
102
|
+
WK8305: [
|
|
103
|
+
{ tool: "kube-linter", rule: "dangling-ingress", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#dangling-ingress", relation: "overlaps" },
|
|
104
|
+
{ tool: "kube-score", rule: "ingress-targets-service", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
105
|
+
],
|
|
106
|
+
WK8405: [
|
|
107
|
+
{ tool: "kube-score", rule: "deployment-has-poddisruptionbudget", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
108
|
+
{ tool: "polaris", rule: "missingPodDisruptionBudget", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
|
|
109
|
+
],
|
|
110
|
+
WK8406: [
|
|
111
|
+
{ tool: "checkov", rule: "CKV_K8S_13", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
|
|
112
|
+
{ tool: "kube-score", rule: "container-resources", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
|
|
113
|
+
{ tool: "kube-linter", rule: "unset-memory-requirements", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#unset-memory-requirements", relation: "overlaps" },
|
|
114
|
+
],
|
|
115
|
+
WK8501: [
|
|
116
|
+
{ tool: "kube-linter", rule: "schema-validation", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#schema-validation", relation: "overlaps" },
|
|
117
|
+
],
|
|
118
|
+
WK8502: [
|
|
119
|
+
{ tool: "kube-linter", rule: "schema-validation", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#schema-validation", relation: "overlaps" },
|
|
120
|
+
],
|
|
121
|
+
WK8503: [
|
|
122
|
+
{ tool: "kube-linter", rule: "env-value-from", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-value-from", relation: "overlaps" },
|
|
123
|
+
],
|
|
124
|
+
WK8505: [
|
|
125
|
+
{ tool: "flux-docs", rule: ".spec.decryption", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#decryption", relation: "overlaps" },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
type ArgoAppStatus,
|
|
6
6
|
type ArgoStatusFetcher,
|
|
7
7
|
} from "./argo";
|
|
8
|
-
// Activity profiles live centrally in
|
|
9
|
-
//
|
|
10
|
-
import {
|
|
8
|
+
// Activity profiles live centrally in core (loadProfiles serves this table);
|
|
9
|
+
// argoSync marks ArgoSyncFailedError non-retryable for this activity.
|
|
10
|
+
import { ACTIVITY_PROFILES } from "@intentius/chant/op/activity-profiles";
|
|
11
11
|
|
|
12
12
|
/** A fetcher that returns a scripted sequence of statuses, repeating the last. */
|
|
13
13
|
function scriptedFetcher(sequence: ArgoAppStatus[]): ArgoStatusFetcher {
|
|
@@ -66,14 +66,13 @@ describe("waitForArgoSync", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
describe("argoSync profile", () => {
|
|
69
|
-
test("is exported with a long timeout
|
|
70
|
-
const p =
|
|
71
|
-
expect(p.
|
|
72
|
-
expect(p.heartbeatTimeout).toBe("60s");
|
|
69
|
+
test("is exported with a long timeout", () => {
|
|
70
|
+
const p = ACTIVITY_PROFILES.argoSync;
|
|
71
|
+
expect(p.timeout).toBe("30m");
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
test("treats ArgoSyncFailedError as non-retryable", () => {
|
|
76
|
-
expect(
|
|
75
|
+
expect(ACTIVITY_PROFILES.argoSync.retry?.nonRetryableErrorTypes).toContain(
|
|
77
76
|
"ArgoSyncFailedError",
|
|
78
77
|
);
|
|
79
78
|
});
|
|
@@ -16,8 +16,8 @@ const execAsync = promisify(exec);
|
|
|
16
16
|
* `ResourceFetcher` for `waitForReady`.
|
|
17
17
|
*
|
|
18
18
|
* It stays **dependency-light** — primitives-only signature (app name /
|
|
19
|
-
* namespace / server), no generated Argo CRD types — so
|
|
20
|
-
*
|
|
19
|
+
* namespace / server), no generated Argo CRD types — so importing the lexicon's
|
|
20
|
+
* activity module at run time stays cheap.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export interface WaitForArgoSyncArgs {
|
|
@@ -36,7 +36,7 @@ export interface WaitForArgoSyncArgs {
|
|
|
36
36
|
insecure?: boolean;
|
|
37
37
|
/** kubectl context (used when `server` is not set). */
|
|
38
38
|
context?: string;
|
|
39
|
-
/** Poll interval in ms (default 15000).
|
|
39
|
+
/** Poll interval in ms (default 15000). */
|
|
40
40
|
intervalMs?: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -113,7 +113,7 @@ export const defaultArgoStatusFetcher: ArgoStatusFetcher = (args, signal) =>
|
|
|
113
113
|
* `ArgoSyncFailedError` if it reaches a terminal unhealthy state (Degraded /
|
|
114
114
|
* Missing).
|
|
115
115
|
*
|
|
116
|
-
* Delegates the poll loop
|
|
116
|
+
* Delegates the poll loop and the ready/terminal evaluation to the
|
|
117
117
|
* generic `waitForReady` using the shared `argoproj.io/Application` readiness
|
|
118
118
|
* spec. The Argo `ArgoStatusFetcher` is adapted into a `ResourceFetcher` that
|
|
119
119
|
* shapes `{health, sync}` into the `status.health.status` / `status.sync.status`
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* k8s Op activities — resolved by the core activity registry when a project's
|
|
3
|
-
* `chant.config.ts` lists the `k8s` lexicon. Relocated from the
|
|
3
|
+
* `chant.config.ts` lists the `k8s` lexicon. Relocated from the hosting lexicon
|
|
4
4
|
* (#809) so Kubernetes-facing imperative activities live with their product:
|
|
5
5
|
* - kubectlApply — server-side apply a rendered manifest (and, since chant
|
|
6
6
|
* #1075, prune chant-owned objects it no longer declares; `applyManifest`
|
|
7
|
-
* is the same work with a report of what it did, which is what
|
|
8
|
-
*
|
|
7
|
+
* is the same work with a report of what it did, which is what core's
|
|
8
|
+
* `nativeApply` dispatcher calls for a kubectl target)
|
|
9
9
|
* - waitForArgoSync — block until an Argo CD Application is Healthy && Synced
|
|
10
10
|
*
|
|
11
11
|
* k3dUp / k3dDown moved again, to the k3d lexicon (chant #1410) — a lexicon
|
|
12
12
|
* owns its own product's activities, and k3d is its own product now. Projects
|
|
13
13
|
* using them list `k3d` in `lexicons`; loadActivities(["k3d"]) provides them.
|
|
14
14
|
*
|
|
15
|
-
* The step builders (kubectlApply, k3dUp, k3dDown) stay in core
|
|
16
|
-
*
|
|
17
|
-
* dependency-light — it shells out to a CLI and does not import the
|
|
18
|
-
* surface — so
|
|
15
|
+
* The step builders (kubectlApply, k3dUp, k3dDown) stay in core and reach
|
|
16
|
+
* authors through `@intentius/chant/op` like the other core builders. Each
|
|
17
|
+
* activity is dependency-light — it shells out to a CLI and does not import the
|
|
18
|
+
* k8s declarable surface — so `loadActivities`
|
|
19
|
+
* (`packages/core/src/op/activity-registry.ts`), which imports this module at
|
|
20
|
+
* run time, pulls in nothing expensive.
|
|
19
21
|
*/
|
|
20
22
|
export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl";
|
|
21
23
|
export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `kubectlApply` over the typed API client (chant #1074, #1075).
|
|
3
3
|
*
|
|
4
|
-
* The activity contract is what
|
|
5
|
-
* arguments and the `Promise<void>` return are asserted
|
|
6
|
-
* behavior. Nothing here spawns a process or reads an ambient
|
|
7
|
-
* which is the acceptance criterion:
|
|
4
|
+
* The activity contract is what an Op step names and core's registry resolves,
|
|
5
|
+
* so the shape of the arguments and the `Promise<void>` return are asserted
|
|
6
|
+
* alongside the new behavior. Nothing here spawns a process or reads an ambient
|
|
7
|
+
* kubeconfig, which is the acceptance criterion: the image a run happens in
|
|
8
|
+
* needs no `kubectl` binary.
|
|
8
9
|
*
|
|
9
10
|
* chant #1075 adds two things to assert: that the field manager is derived
|
|
10
11
|
* from the project's `ownership.stack` rather than hardcoded, and that the
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* chant #1074 moved this off `kubectl apply -f`. The activity contract is
|
|
5
5
|
* unchanged (a manifest path, an optional context, `Promise<void>`, the
|
|
6
|
-
* `longInfra` profile
|
|
7
|
-
*
|
|
8
|
-
* reason.
|
|
6
|
+
* `longInfra` profile) because an Op step names the activity by its export
|
|
7
|
+
* name and core's registry resolves it there; what changed is underneath. The
|
|
8
|
+
* name is kept for the same reason.
|
|
9
9
|
*
|
|
10
10
|
* chant #1075 finished the job on two axes:
|
|
11
11
|
*
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import { loadAll } from "js-yaml";
|
|
37
|
-
import { safeHeartbeat } from "@intentius/chant/op";
|
|
38
37
|
import { loadChantConfig, resolveOwnershipStack } from "@intentius/chant/config";
|
|
39
38
|
import {
|
|
40
39
|
hasOwnershipMarker,
|
|
@@ -50,15 +49,15 @@ import { isGeneratedOnce } from "../../secret-labels";
|
|
|
50
49
|
/**
|
|
51
50
|
* How the apply treats chant-owned objects that are no longer declared. The
|
|
52
51
|
* same three values `nativeApply` has always taken; `gated` differs from
|
|
53
|
-
* `owned-only` only in that
|
|
54
|
-
* the composite's business, not this activity's.
|
|
52
|
+
* `owned-only` only in that an approval gate runs before this activity does,
|
|
53
|
+
* which is the composite's business, not this activity's.
|
|
55
54
|
*/
|
|
56
55
|
export type ApplyDeleteMode = "never" | "owned-only" | "gated";
|
|
57
56
|
|
|
58
57
|
export interface KubectlApplyArgs {
|
|
59
58
|
/**
|
|
60
59
|
* Path to a manifest file, or a directory of them. With `documents` given,
|
|
61
|
-
* this becomes only the human-facing label the
|
|
60
|
+
* this becomes only the human-facing label the step's log lines carry
|
|
62
61
|
* (e.g. `kustomize:<dir>`), and nothing is read from disk.
|
|
63
62
|
*/
|
|
64
63
|
manifest: string;
|
|
@@ -310,74 +309,61 @@ export async function applyManifest(
|
|
|
310
309
|
): Promise<ApplyManifestResult> {
|
|
311
310
|
const documents = args.documents ?? readManifestDocuments(args.manifest);
|
|
312
311
|
const { fieldManager, stack } = await resolveApplyIdentity(args);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const { client } = await connect({
|
|
319
|
-
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
320
|
-
...(args.context !== undefined ? { context: args.context } : {}),
|
|
321
|
-
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
322
|
-
});
|
|
312
|
+
const { client } = await connect({
|
|
313
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
314
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
315
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
316
|
+
});
|
|
323
317
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
-
} catch (err) {
|
|
336
|
-
// "chant never forces a conflict on its own" is about taking fields
|
|
337
|
-
// from ANOTHER tool. A conflict where every contested field is owned
|
|
338
|
-
// by another `chant:*` manager is chant contesting itself — the
|
|
339
|
-
// ownership-stack → unit-stack label migration, or a renamed deploy
|
|
340
|
-
// unit — and refusing that forever would strand every estate applied
|
|
341
|
-
// before the rename with no non-force path back. Retake those fields
|
|
342
|
-
// deliberately, once, and only when no foreign manager is involved.
|
|
343
|
-
if (!isChantSelfConflict(err)) throw err;
|
|
344
|
-
result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
|
|
345
|
-
}
|
|
346
|
-
const ref: AppliedRef = {
|
|
347
|
-
apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
|
|
348
|
-
kind: String(result.kind ?? document.kind ?? ""),
|
|
349
|
-
name: String(result.metadata?.name ?? ""),
|
|
350
|
-
...(result.metadata?.namespace !== undefined
|
|
351
|
-
? { namespace: String(result.metadata.namespace) }
|
|
352
|
-
: {}),
|
|
353
|
-
};
|
|
354
|
-
applied.push(ref);
|
|
355
|
-
safeHeartbeat({
|
|
356
|
-
step: "kubectl apply",
|
|
357
|
-
manifest: args.manifest,
|
|
358
|
-
applied: `${ref.kind}/${ref.name}`,
|
|
318
|
+
const applied: AppliedRef[] = [];
|
|
319
|
+
for (const document of documents) {
|
|
320
|
+
const stamped = stampOwnership(document as K8sObject, stack);
|
|
321
|
+
let result: K8sObject;
|
|
322
|
+
try {
|
|
323
|
+
result = await client.apply(stamped, {
|
|
324
|
+
fieldManager,
|
|
325
|
+
force: args.force ?? false,
|
|
326
|
+
dryRun: args.dryRun,
|
|
327
|
+
signal,
|
|
359
328
|
});
|
|
360
|
-
|
|
329
|
+
} catch (err) {
|
|
330
|
+
// "chant never forces a conflict on its own" is about taking fields
|
|
331
|
+
// from ANOTHER tool. A conflict where every contested field is owned
|
|
332
|
+
// by another `chant:*` manager is chant contesting itself — the
|
|
333
|
+
// ownership-stack → unit-stack label migration, or a renamed deploy
|
|
334
|
+
// unit — and refusing that forever would strand every estate applied
|
|
335
|
+
// before the rename with no non-force path back. Retake those fields
|
|
336
|
+
// deliberately, once, and only when no foreign manager is involved.
|
|
337
|
+
if (!isChantSelfConflict(err)) throw err;
|
|
338
|
+
result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
|
|
361
339
|
}
|
|
340
|
+
const ref: AppliedRef = {
|
|
341
|
+
apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
|
|
342
|
+
kind: String(result.kind ?? document.kind ?? ""),
|
|
343
|
+
name: String(result.metadata?.name ?? ""),
|
|
344
|
+
...(result.metadata?.namespace !== undefined
|
|
345
|
+
? { namespace: String(result.metadata.namespace) }
|
|
346
|
+
: {}),
|
|
347
|
+
};
|
|
348
|
+
applied.push(ref);
|
|
349
|
+
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
|
|
350
|
+
}
|
|
362
351
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
352
|
+
const deleteMode = args.deleteMode ?? "never";
|
|
353
|
+
const { pruned, retained } =
|
|
354
|
+
deleteMode === "never" || args.dryRun
|
|
355
|
+
? { pruned: [], retained: [] }
|
|
356
|
+
: await pruneOrphans(client, applied, {
|
|
357
|
+
...(stack !== undefined ? { stack } : {}),
|
|
358
|
+
signal,
|
|
359
|
+
});
|
|
371
360
|
|
|
372
|
-
|
|
373
|
-
} finally {
|
|
374
|
-
clearInterval(heartbeatInterval);
|
|
375
|
-
}
|
|
361
|
+
return { fieldManager, applied, pruned, retained };
|
|
376
362
|
}
|
|
377
363
|
|
|
378
364
|
/**
|
|
379
365
|
* Apply every document in `args.manifest`.
|
|
380
|
-
* Uses longInfra profile
|
|
366
|
+
* Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
|
|
381
367
|
*/
|
|
382
368
|
export async function kubectlApply(
|
|
383
369
|
args: KubectlApplyArgs,
|
|
@@ -524,7 +510,6 @@ async function pruneOrphans(
|
|
|
524
510
|
for (const ref of candidates) {
|
|
525
511
|
await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
|
|
526
512
|
pruned.push(ref);
|
|
527
|
-
safeHeartbeat({ step: "prune", pruned: `${ref.kind}/${ref.name}` });
|
|
528
513
|
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
|
|
529
514
|
}
|
|
530
515
|
return { pruned, retained };
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ReadinessSpec,
|
|
12
12
|
} from "./wait-for-ready";
|
|
13
13
|
// The k8sWait profile marks ReadinessFailedError non-retryable for this activity.
|
|
14
|
-
import {
|
|
14
|
+
import { ACTIVITY_PROFILES } from "@intentius/chant/op/activity-profiles";
|
|
15
15
|
import { fakeCluster, objectKey } from "../../api/fake-cluster";
|
|
16
16
|
|
|
17
17
|
/** A fetcher returning a scripted sequence of objects, repeating the last. */
|
|
@@ -156,7 +156,7 @@ describe("waitForReady", () => {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test("k8sWait marks ReadinessFailedError non-retryable", () => {
|
|
159
|
-
expect(
|
|
159
|
+
expect(ACTIVITY_PROFILES.k8sWait.retry?.nonRetryableErrorTypes).toContain("ReadinessFailedError");
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
test("explicit spec wins over the registry", async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sleep } from "@intentius/chant/op";
|
|
2
2
|
import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -6,8 +6,8 @@ import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
|
6
6
|
* ready, driven by a data-only **readiness spec** rather than per-CRD code.
|
|
7
7
|
*
|
|
8
8
|
* Like `waitForArgoSync`, this activity is intentionally **dependency-light**:
|
|
9
|
-
* its signature is primitives + a plain readiness spec, so
|
|
10
|
-
* loads
|
|
9
|
+
* its signature is primitives + a plain readiness spec, so the activity module
|
|
10
|
+
* loads without pulling in the generated CRD declarable surface. It reads the
|
|
11
11
|
* resource and evaluates the spec's predicates. It generalizes the bespoke
|
|
12
12
|
* `waitForArgoSync` / `waitForStack` waits — see #365.
|
|
13
13
|
*
|
|
@@ -271,7 +271,7 @@ export interface WaitForReadyArgs {
|
|
|
271
271
|
group?: string;
|
|
272
272
|
/** Explicit readiness spec — wins over the registry/default. */
|
|
273
273
|
spec?: ReadinessSpec;
|
|
274
|
-
/** Poll interval in ms (default 15000).
|
|
274
|
+
/** Poll interval in ms (default 15000). */
|
|
275
275
|
intervalMs?: number;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -335,8 +335,9 @@ export const defaultResourceFetcher: ResourceFetcher = (args, signal) => apiReso
|
|
|
335
335
|
|
|
336
336
|
/**
|
|
337
337
|
* Poll until the resource satisfies its readiness spec. Throws
|
|
338
|
-
* `ReadinessFailedError` on a terminal state
|
|
339
|
-
*
|
|
338
|
+
* `ReadinessFailedError` on a terminal state, which the `k8sWait` profile lists
|
|
339
|
+
* as non-retryable, so a resource that will never become ready fails on the
|
|
340
|
+
* first attempt instead of burning the profile's 15m timeout three times.
|
|
340
341
|
*
|
|
341
342
|
* @param fetcher injectable reader (defaults to kubectl). Tests pass a fake to
|
|
342
343
|
* drive not-ready → ready / terminal transitions.
|
|
@@ -355,7 +356,6 @@ export async function waitForReady(
|
|
|
355
356
|
attempt++;
|
|
356
357
|
|
|
357
358
|
const obj = await fetcher(args, signal);
|
|
358
|
-
safeHeartbeat({ step: "waitForReady", kind: args.kind, name: args.name, attempt });
|
|
359
359
|
|
|
360
360
|
const term = firstTerminal(obj, spec);
|
|
361
361
|
if (term) {
|