@intentius/chant-lexicon-k8s 0.57.0 → 0.59.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/fake-cluster.d.ts +7 -6
- package/dist/api/fake-cluster.d.ts.map +1 -1
- package/dist/api/sweep-types.d.ts +1 -2
- package/dist/api/sweep-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/composites/cron-schedule.d.ts +26 -0
- package/dist/composites/cron-schedule.d.ts.map +1 -0
- package/dist/composites/cron-workload.d.ts +1 -1
- package/dist/composites/cron-workload.d.ts.map +1 -1
- package/dist/composites/operator-stack.d.ts +20 -19
- package/dist/composites/operator-stack.d.ts.map +1 -1
- package/dist/config-schema.d.ts +3 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +26 -4
- package/dist/config.d.ts.map +1 -1
- 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/describe-resources.d.ts.map +1 -1
- package/dist/effect-receipt-row.d.ts +164 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/export-resources.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- 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/lint/post-synth/wk8505.d.ts +3 -1
- package/dist/lint/post-synth/wk8505.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -1
- package/dist/okf/rules/WK8505.md +2 -2
- package/dist/okf/types/Kustomization.md +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 +15 -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/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +113 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/wk8505.ts +4 -2
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-k8s-argo.md +27 -13
- package/dist/subscribe-changes.d.ts +84 -0
- package/dist/subscribe-changes.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/api/fake-cluster.ts +17 -4
- package/src/api/sweep-types.ts +1 -2
- package/src/codegen/docs.ts +7 -0
- package/src/composites/composites.test.ts +72 -3
- package/src/composites/cron-schedule.ts +47 -0
- package/src/composites/cron-workload.ts +4 -1
- package/src/composites/operator-stack.ts +25 -24
- package/src/config-schema.ts +5 -0
- package/src/config.ts +27 -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 +54 -28
- package/src/describe-resources.ts +31 -7
- package/src/effect-receipt-row.test.ts +285 -0
- package/src/effect-receipt-row.ts +268 -0
- package/src/export-resources.ts +1 -1
- package/src/index.ts +22 -0
- package/src/lint/audit-catalog.ts +6 -2
- package/src/lint/audit-lineage.ts +127 -0
- package/src/lint/post-synth/wk8505.ts +4 -2
- package/src/op/activities/argo.test.ts +7 -8
- package/src/op/activities/argo.ts +4 -4
- package/src/op/activities/index.ts +23 -7
- package/src/op/activities/kubectl.test.ts +43 -4
- package/src/op/activities/kubectl.ts +67 -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 +16 -1
- package/src/receipt-store.test.ts +380 -0
- package/src/receipt-store.ts +290 -0
- package/src/serializer.test.ts +26 -0
- package/src/serializer.ts +92 -1
- package/src/skills/chant-k8s-argo.md +27 -13
- package/src/subscribe-changes.test.ts +368 -0
- package/src/subscribe-changes.ts +210 -0
|
@@ -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
|
+
};
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* invocations, so "no Kustomization in THIS build sets spec.decryption" can
|
|
16
16
|
* mean "wired up in the other build" as easily as "forgotten" — the design
|
|
17
17
|
* doc's §4 explains why no path-to-build-target join exists to tell those
|
|
18
|
-
* apart.
|
|
18
|
+
* apart. Single-build-root scoped, tracked by chant #1939 as a candidate for
|
|
19
|
+
* an opt-in project-level mode; promote to error if that join ever becomes
|
|
20
|
+
* available.
|
|
19
21
|
*
|
|
20
22
|
* Fires only on claims that actually resolved (`problems.length === 0`, via
|
|
21
23
|
* `resolveEncryptedSecretClaims`), not on every raw declaration. An
|
|
@@ -39,7 +41,7 @@ function fluxKustomizations(manifests: K8sManifest[]): K8sManifest[] {
|
|
|
39
41
|
export const wk8505: PostSynthCheck = {
|
|
40
42
|
id: "WK8505",
|
|
41
43
|
description:
|
|
42
|
-
"committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it",
|
|
44
|
+
"committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it. Sees one build root at a time, so it goes silent when that Kustomization lives in a different build root (chant #1939).",
|
|
43
45
|
|
|
44
46
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
47
|
const kustomizations = fluxKustomizations(allManifests(ctx));
|
|
@@ -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";
|
|
@@ -61,3 +63,17 @@ export type {
|
|
|
61
63
|
// truth — see `lexicons/k8s/src/config.ts` for the config shape.
|
|
62
64
|
export { resolveClusterTarget, ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
|
|
63
65
|
export type { ResolvedClusterTarget, K8sClusterProfile, K8sConfigShape } from "@intentius/chant/kubectl-context";
|
|
66
|
+
|
|
67
|
+
// Effect-receipt activities (#2074): core's receipt seam (#1834) bound to
|
|
68
|
+
// this lexicon's ConfigMap-backed store (../../receipt-store.ts), the same
|
|
69
|
+
// way the aws lexicon binds its SSM store (#1835). Re-exported individually:
|
|
70
|
+
// `receiptRead`/`receiptWrite` serve the `effect()` step's
|
|
71
|
+
// read-compare-run-write, `receiptStaleness` serves WatchOp's read-only
|
|
72
|
+
// staleness reporting.
|
|
73
|
+
import { receiptActivities } from "@intentius/chant/op/receipt-store";
|
|
74
|
+
import { k8sReceiptStore } from "../../receipt-store";
|
|
75
|
+
|
|
76
|
+
const boundReceiptActivities = receiptActivities(k8sReceiptStore());
|
|
77
|
+
export const receiptRead = boundReceiptActivities.receiptRead;
|
|
78
|
+
export const receiptWrite = boundReceiptActivities.receiptWrite;
|
|
79
|
+
export const receiptStaleness = boundReceiptActivities.receiptStaleness;
|
|
@@ -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
|
|
@@ -493,6 +494,44 @@ describe("the ownership-scoped prune (chant #1075)", () => {
|
|
|
493
494
|
expect(deletes).toEqual(["/api/v1/namespaces/prod/secrets/stale-config"]);
|
|
494
495
|
});
|
|
495
496
|
|
|
497
|
+
test("an effect receipt ConfigMap is excluded from the prunable set and reported retained (#2074)", async () => {
|
|
498
|
+
const cluster = fakeCluster({
|
|
499
|
+
respond: echoApplies,
|
|
500
|
+
objects: {
|
|
501
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: ownedObject("apps/v1", "Deployment", "web", "prod"),
|
|
502
|
+
[objectKey("v1", "ConfigMap", "chant-receipt.demo.prod.db-seed", "prod")]: ownedObject(
|
|
503
|
+
"v1",
|
|
504
|
+
"ConfigMap",
|
|
505
|
+
"chant-receipt.demo.prod.db-seed",
|
|
506
|
+
"prod",
|
|
507
|
+
{
|
|
508
|
+
metadata: {
|
|
509
|
+
labels: {
|
|
510
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
511
|
+
"chant.intentius.io/effect-receipt": "db-seed",
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
),
|
|
516
|
+
[objectKey("v1", "ConfigMap", "stale-config", "prod")]: ownedObject("v1", "ConfigMap", "stale-config", "prod"),
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
const result = await applyManifest(
|
|
520
|
+
{ manifest: manifest(), deleteMode: "owned-only" },
|
|
521
|
+
undefined,
|
|
522
|
+
cluster.connector,
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
expect(result.retained).toEqual([
|
|
526
|
+
{ apiVersion: "v1", kind: "ConfigMap", name: "chant-receipt.demo.prod.db-seed", namespace: "prod" },
|
|
527
|
+
]);
|
|
528
|
+
expect(result.pruned).toEqual([
|
|
529
|
+
{ apiVersion: "v1", kind: "ConfigMap", name: "stale-config", namespace: "prod" },
|
|
530
|
+
]);
|
|
531
|
+
const deletes = cluster.layer.requests.filter((r) => r.method === "DELETE").map((r) => r.path);
|
|
532
|
+
expect(deletes).toEqual(["/api/v1/namespaces/prod/configmaps/stale-config"]);
|
|
533
|
+
});
|
|
534
|
+
|
|
496
535
|
test("an object without chant's marker is never a candidate", async () => {
|
|
497
536
|
const cluster = clusterWithOrphans();
|
|
498
537
|
await applyManifest({ manifest: manifest(), deleteMode: "owned-only" }, undefined, cluster.connector);
|
|
@@ -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,
|
|
@@ -46,19 +45,20 @@ import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
|
46
45
|
import { operationFor } from "../../api/operation-surface";
|
|
47
46
|
import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types";
|
|
48
47
|
import { isGeneratedOnce } from "../../secret-labels";
|
|
48
|
+
import { isEffectReceiptObject } from "../../effect-receipt-row";
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* How the apply treats chant-owned objects that are no longer declared. The
|
|
52
52
|
* 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.
|
|
53
|
+
* `owned-only` only in that an approval gate runs before this activity does,
|
|
54
|
+
* which is the composite's business, not this activity's.
|
|
55
55
|
*/
|
|
56
56
|
export type ApplyDeleteMode = "never" | "owned-only" | "gated";
|
|
57
57
|
|
|
58
58
|
export interface KubectlApplyArgs {
|
|
59
59
|
/**
|
|
60
60
|
* Path to a manifest file, or a directory of them. With `documents` given,
|
|
61
|
-
* this becomes only the human-facing label the
|
|
61
|
+
* this becomes only the human-facing label the step's log lines carry
|
|
62
62
|
* (e.g. `kustomize:<dir>`), and nothing is read from disk.
|
|
63
63
|
*/
|
|
64
64
|
manifest: string;
|
|
@@ -310,74 +310,61 @@ export async function applyManifest(
|
|
|
310
310
|
): Promise<ApplyManifestResult> {
|
|
311
311
|
const documents = args.documents ?? readManifestDocuments(args.manifest);
|
|
312
312
|
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
|
-
});
|
|
313
|
+
const { client } = await connect({
|
|
314
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
315
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
316
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
317
|
+
});
|
|
323
318
|
|
|
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}`,
|
|
319
|
+
const applied: AppliedRef[] = [];
|
|
320
|
+
for (const document of documents) {
|
|
321
|
+
const stamped = stampOwnership(document as K8sObject, stack);
|
|
322
|
+
let result: K8sObject;
|
|
323
|
+
try {
|
|
324
|
+
result = await client.apply(stamped, {
|
|
325
|
+
fieldManager,
|
|
326
|
+
force: args.force ?? false,
|
|
327
|
+
dryRun: args.dryRun,
|
|
328
|
+
signal,
|
|
359
329
|
});
|
|
360
|
-
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// "chant never forces a conflict on its own" is about taking fields
|
|
332
|
+
// from ANOTHER tool. A conflict where every contested field is owned
|
|
333
|
+
// by another `chant:*` manager is chant contesting itself — the
|
|
334
|
+
// ownership-stack → unit-stack label migration, or a renamed deploy
|
|
335
|
+
// unit — and refusing that forever would strand every estate applied
|
|
336
|
+
// before the rename with no non-force path back. Retake those fields
|
|
337
|
+
// deliberately, once, and only when no foreign manager is involved.
|
|
338
|
+
if (!isChantSelfConflict(err)) throw err;
|
|
339
|
+
result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
|
|
361
340
|
}
|
|
341
|
+
const ref: AppliedRef = {
|
|
342
|
+
apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
|
|
343
|
+
kind: String(result.kind ?? document.kind ?? ""),
|
|
344
|
+
name: String(result.metadata?.name ?? ""),
|
|
345
|
+
...(result.metadata?.namespace !== undefined
|
|
346
|
+
? { namespace: String(result.metadata.namespace) }
|
|
347
|
+
: {}),
|
|
348
|
+
};
|
|
349
|
+
applied.push(ref);
|
|
350
|
+
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
|
|
351
|
+
}
|
|
362
352
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
353
|
+
const deleteMode = args.deleteMode ?? "never";
|
|
354
|
+
const { pruned, retained } =
|
|
355
|
+
deleteMode === "never" || args.dryRun
|
|
356
|
+
? { pruned: [], retained: [] }
|
|
357
|
+
: await pruneOrphans(client, applied, {
|
|
358
|
+
...(stack !== undefined ? { stack } : {}),
|
|
359
|
+
signal,
|
|
360
|
+
});
|
|
371
361
|
|
|
372
|
-
|
|
373
|
-
} finally {
|
|
374
|
-
clearInterval(heartbeatInterval);
|
|
375
|
-
}
|
|
362
|
+
return { fieldManager, applied, pruned, retained };
|
|
376
363
|
}
|
|
377
364
|
|
|
378
365
|
/**
|
|
379
366
|
* Apply every document in `args.manifest`.
|
|
380
|
-
* Uses longInfra profile
|
|
367
|
+
* Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
|
|
381
368
|
*/
|
|
382
369
|
export async function kubectlApply(
|
|
383
370
|
args: KubectlApplyArgs,
|
|
@@ -517,6 +504,20 @@ async function pruneOrphans(
|
|
|
517
504
|
);
|
|
518
505
|
continue;
|
|
519
506
|
}
|
|
507
|
+
// An effect receipt never enters the prunable set either (#2074, epic
|
|
508
|
+
// #1703 decision 3). It is chant-owned and is never in ANY apply set,
|
|
509
|
+
// the `effect()` step being its sole writer, which is exactly the shape
|
|
510
|
+
// this sweep deletes. Pruning one would tell the next plan that an effect
|
|
511
|
+
// never ran and re-fire it, or worse, be re-stamped by nothing and leave
|
|
512
|
+
// the estate re-running a migration on every apply.
|
|
513
|
+
if (target.kind === "ConfigMap" && isEffectReceiptObject(item.metadata?.labels)) {
|
|
514
|
+
retained.push(ref);
|
|
515
|
+
console.log(
|
|
516
|
+
`${ref.apiVersion} ${ref.kind}/${ref.name} retained (effect receipt: owned and never declared ` +
|
|
517
|
+
`in an apply set, but never pruned; the effect() step is its sole writer)`,
|
|
518
|
+
);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
520
521
|
candidates.push(ref);
|
|
521
522
|
}
|
|
522
523
|
|
|
@@ -524,7 +525,6 @@ async function pruneOrphans(
|
|
|
524
525
|
for (const ref of candidates) {
|
|
525
526
|
await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
|
|
526
527
|
pruned.push(ref);
|
|
527
|
-
safeHeartbeat({ step: "prune", pruned: `${ref.kind}/${ref.name}` });
|
|
528
528
|
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
|
|
529
529
|
}
|
|
530
530
|
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) {
|
package/src/op/builders.ts
CHANGED
|
@@ -18,20 +18,20 @@
|
|
|
18
18
|
* earlier step's declared output.
|
|
19
19
|
*
|
|
20
20
|
* `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
|
|
21
|
-
* `@intentius/chant/op`,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* `@intentius/chant/op`, the lexicon-agnostic single-import barrel) are
|
|
22
|
+
* UNCHANGED and produce byte-identical `ActivityStep` output for the same
|
|
23
|
+
* inputs — these are purely additive. Deliberately not swapped into core's
|
|
24
|
+
* barrel: that would make `@intentius/chant` depend on this package (and on
|
|
25
|
+
* helm, for `helmInstall`) at runtime, so a project that never touches
|
|
26
|
+
* Kubernetes would install both just to import anything from `op`. That is
|
|
27
|
+
* exactly the "core stays product-agnostic" property the
|
|
28
28
|
* `op/activities/index.ts` module docs across this repo call out on purpose
|
|
29
|
-
* (activities
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* already depends on; existing `@intentius/chant
|
|
34
|
-
*
|
|
29
|
+
* (activities live in per-product lexicons for this reason; the step-builder
|
|
30
|
+
* layer shouldn't reintroduce the coupling from the other direction). An
|
|
31
|
+
* author who wants the typed surface imports it from here —
|
|
32
|
+
* `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
|
|
33
|
+
* already depends on; existing `@intentius/chant/op` imports keep working
|
|
34
|
+
* exactly as before, opting in only if the import is changed.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import {
|