@intentius/chant-lexicon-k8s 0.46.0 → 0.50.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/components/kubectl-apply.d.ts +5 -0
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/composites/flux-app.d.ts +12 -0
- package/dist/composites/flux-app.d.ts.map +1 -1
- package/dist/composites/index.d.ts +8 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/inference-service.d.ts +86 -0
- package/dist/composites/inference-service.d.ts.map +1 -0
- package/dist/composites/model.d.ts +80 -0
- package/dist/composites/model.d.ts.map +1 -0
- package/dist/composites/operator-stack.d.ts +183 -0
- package/dist/composites/operator-stack.d.ts.map +1 -0
- package/dist/composites/vllm-serving-runtime.d.ts +100 -0
- package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
- package/dist/crd/crd-sources.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/generated/index.d.ts +167 -40
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/group-namespace.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +41 -33
- package/dist/kube/apply.d.ts.map +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/k8s-helpers.d.ts +56 -6
- package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
- package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8404.d.ts +12 -0
- package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8405.d.ts +13 -0
- package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8406.d.ts +12 -0
- package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8407.d.ts +21 -0
- package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8503.d.ts +39 -0
- package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8504.d.ts +28 -0
- package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8505.d.ts +28 -0
- package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +1039 -68
- package/dist/okf/index.md +26 -2
- package/dist/okf/rules/FLUX002.md +0 -1
- package/dist/okf/rules/WK8404.md +11 -0
- package/dist/okf/rules/WK8405.md +17 -0
- package/dist/okf/rules/WK8406.md +11 -0
- package/dist/okf/rules/WK8407.md +15 -0
- package/dist/okf/rules/WK8503.md +17 -0
- package/dist/okf/rules/WK8504.md +11 -0
- package/dist/okf/rules/WK8505.md +15 -0
- package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
- package/dist/okf/types/AWSManagedCluster.md +14 -0
- package/dist/okf/types/AWSManagedControlPlane.md +14 -0
- package/dist/okf/types/AWSManagedMachinePool.md +14 -0
- package/dist/okf/types/CAPICluster.md +14 -0
- package/dist/okf/types/Certificate.md +1 -0
- package/dist/okf/types/ClusterResourceSet.md +14 -0
- package/dist/okf/types/ClusterServingRuntime.md +14 -0
- package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
- package/dist/okf/types/DBInstance.md +14 -0
- package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
- package/dist/okf/types/HelmChartProxy.md +14 -0
- package/dist/okf/types/IamRole.md +14 -0
- package/dist/okf/types/InferenceService.md +19 -0
- package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
- package/dist/okf/types/InfisicalSecret.md +4 -0
- package/dist/okf/types/Kustomization.md +1 -0
- package/dist/okf/types/MachinePool.md +14 -0
- package/dist/okf/types/PDB.md +1 -0
- package/dist/okf/types/PodIdentityAssociation.md +14 -0
- package/dist/okf/types/Policy.md +14 -0
- package/dist/okf/types/S3Bucket.md +14 -0
- package/dist/okf/types/ServingRuntime.md +14 -0
- package/dist/okf/types/StatefulSet.md +1 -0
- package/dist/okf/types/User.md +14 -0
- package/dist/op/activities/ensure-secret.d.ts +41 -0
- package/dist/op/activities/ensure-secret.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +9 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/op/builders.d.ts +69 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/argo-helpers.ts +2 -6
- package/dist/rules/crd-schema-helpers.ts +4 -6
- package/dist/rules/k8s-helpers.ts +87 -24
- package/dist/rules/sops-helpers.ts +76 -0
- package/dist/rules/wk8005.ts +27 -32
- package/dist/rules/wk8006.ts +33 -38
- package/dist/rules/wk8041.ts +19 -24
- package/dist/rules/wk8042.ts +17 -22
- package/dist/rules/wk8101.ts +29 -34
- package/dist/rules/wk8102.ts +13 -18
- package/dist/rules/wk8103.ts +17 -22
- package/dist/rules/wk8104.ts +30 -35
- package/dist/rules/wk8105.ts +16 -21
- package/dist/rules/wk8201.ts +26 -31
- package/dist/rules/wk8202.ts +17 -22
- package/dist/rules/wk8203.ts +17 -22
- package/dist/rules/wk8204.ts +46 -51
- package/dist/rules/wk8205.ts +27 -32
- package/dist/rules/wk8207.ts +14 -19
- package/dist/rules/wk8208.ts +14 -19
- package/dist/rules/wk8209.ts +14 -19
- package/dist/rules/wk8301.ts +25 -30
- package/dist/rules/wk8302.ts +19 -24
- package/dist/rules/wk8303.ts +23 -26
- package/dist/rules/wk8304.ts +35 -40
- package/dist/rules/wk8305.ts +39 -42
- package/dist/rules/wk8306.ts +20 -25
- package/dist/rules/wk8401.ts +59 -64
- package/dist/rules/wk8402.ts +13 -18
- package/dist/rules/wk8403.ts +32 -37
- package/dist/rules/wk8404.ts +49 -0
- package/dist/rules/wk8405.ts +85 -0
- package/dist/rules/wk8406.ts +55 -0
- package/dist/rules/wk8407.ts +66 -0
- package/dist/rules/wk8503.ts +220 -0
- package/dist/rules/wk8504.ts +51 -0
- package/dist/rules/wk8505.ts +63 -0
- package/dist/secret-labels.d.ts +31 -0
- package/dist/secret-labels.d.ts.map +1 -0
- package/dist/secret-store.d.ts +73 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sops/encrypted-secret-file.d.ts +52 -0
- package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
- package/dist/sops/entity.d.ts +31 -0
- package/dist/sops/entity.d.ts.map +1 -0
- package/dist/teardown.d.ts.map +1 -1
- package/dist/types/index.d.ts +261 -9
- package/package.json +4 -4
- package/src/components/kubectl-apply.ts +5 -0
- package/src/composites/composites.test.ts +556 -0
- package/src/composites/flux-app.ts +23 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/inference-service.ts +140 -0
- package/src/composites/model.ts +150 -0
- package/src/composites/operator-stack.ts +385 -0
- package/src/composites/vllm-serving-runtime.test.ts +167 -0
- package/src/composites/vllm-serving-runtime.ts +196 -0
- package/src/crd/cnpg.test.ts +8 -3
- package/src/crd/crd-sources.ts +183 -0
- package/src/crd/parser.test.ts +14 -0
- package/src/deep-observe-hooks.ts +24 -0
- package/src/deep-observe.test.ts +89 -0
- package/src/generated/index.d.ts +261 -9
- package/src/generated/index.ts +167 -40
- package/src/generated/lexicon-k8s.json +1039 -68
- package/src/generated/operations.json +272 -0
- package/src/group-namespace.test.ts +31 -0
- package/src/group-namespace.ts +13 -0
- package/src/index.ts +17 -0
- package/src/kube/apply.ts +5 -0
- package/src/lint/audit-catalog.ts +7 -0
- package/src/lint/post-synth/argo-helpers.ts +2 -6
- package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
- package/src/lint/post-synth/k8s-helpers.ts +87 -24
- package/src/lint/post-synth/post-synth.test.ts +826 -0
- package/src/lint/post-synth/sops-helpers.ts +76 -0
- package/src/lint/post-synth/wk8005.ts +27 -32
- package/src/lint/post-synth/wk8006.ts +33 -38
- package/src/lint/post-synth/wk8041.ts +19 -24
- package/src/lint/post-synth/wk8042.ts +17 -22
- package/src/lint/post-synth/wk8101.ts +29 -34
- package/src/lint/post-synth/wk8102.ts +13 -18
- package/src/lint/post-synth/wk8103.ts +17 -22
- package/src/lint/post-synth/wk8104.ts +30 -35
- package/src/lint/post-synth/wk8105.ts +16 -21
- package/src/lint/post-synth/wk8201.ts +26 -31
- package/src/lint/post-synth/wk8202.ts +17 -22
- package/src/lint/post-synth/wk8203.ts +17 -22
- package/src/lint/post-synth/wk8204.ts +46 -51
- package/src/lint/post-synth/wk8205.ts +27 -32
- package/src/lint/post-synth/wk8207.ts +14 -19
- package/src/lint/post-synth/wk8208.ts +14 -19
- package/src/lint/post-synth/wk8209.ts +14 -19
- package/src/lint/post-synth/wk8301.ts +25 -30
- package/src/lint/post-synth/wk8302.ts +19 -24
- package/src/lint/post-synth/wk8303.ts +23 -26
- package/src/lint/post-synth/wk8304.ts +35 -40
- package/src/lint/post-synth/wk8305.ts +39 -42
- package/src/lint/post-synth/wk8306.ts +20 -25
- package/src/lint/post-synth/wk8401.ts +59 -64
- package/src/lint/post-synth/wk8402.ts +13 -18
- package/src/lint/post-synth/wk8403.ts +32 -37
- package/src/lint/post-synth/wk8404.ts +49 -0
- package/src/lint/post-synth/wk8405.ts +85 -0
- package/src/lint/post-synth/wk8406.ts +55 -0
- package/src/lint/post-synth/wk8407.ts +66 -0
- package/src/lint/post-synth/wk8503.ts +220 -0
- package/src/lint/post-synth/wk8504.ts +51 -0
- package/src/lint/post-synth/wk8505.ts +63 -0
- package/src/op/activities/ensure-secret.ts +71 -0
- package/src/op/activities/index.ts +7 -0
- package/src/op/activities/kubectl.test.ts +32 -0
- package/src/op/activities/kubectl.ts +28 -5
- package/src/op/activities/wait-for-ready.test.ts +34 -0
- package/src/op/activities/wait-for-ready.ts +26 -0
- package/src/op/builders.test.ts +82 -0
- package/src/op/builders.ts +95 -0
- package/src/plugin.ts +21 -2
- package/src/secret-labels.ts +36 -0
- package/src/secret-store.test.ts +184 -0
- package/src/secret-store.ts +165 -0
- package/src/serializer.ts +41 -2
- package/src/sops/encrypted-secret-file.test.ts +333 -0
- package/src/sops/encrypted-secret-file.ts +264 -0
- package/src/sops/entity.ts +53 -0
- package/src/teardown.test.ts +36 -0
- package/src/teardown.ts +14 -0
- package/src/testdata/sops/db-credentials.sops.yaml +23 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8505: committed-encrypted secret with no Flux decryption wiring
|
|
3
|
+
*
|
|
4
|
+
* Flux is the thing that actually decrypts a committed-encrypted secret's
|
|
5
|
+
* sidecar, via `spec.decryption` on the reconciling `Kustomization`
|
|
6
|
+
* (`FluxAppFor`'s `decryption` option). Nothing forces the two calls to be
|
|
7
|
+
* wired together: `declareSecret({ provenance: "committed-encrypted" })` and
|
|
8
|
+
* `FluxAppFor(...)` are independent, so it is easy to add the ciphertext
|
|
9
|
+
* without adding the wiring — the build succeeds, WK8504 passes because the
|
|
10
|
+
* file resolves cleanly, and Flux still applies the sidecar as raw
|
|
11
|
+
* `ENC[...]` bytes because nothing told it to decrypt first.
|
|
12
|
+
*
|
|
13
|
+
* Warning, not error. The reference project shape (`examples/flux-apps`)
|
|
14
|
+
* runs the workload build and the Flux build as separate `chant build`
|
|
15
|
+
* invocations, so "no Kustomization in THIS build sets spec.decryption" can
|
|
16
|
+
* mean "wired up in the other build" as easily as "forgotten" — the design
|
|
17
|
+
* doc's §4 explains why no path-to-build-target join exists to tell those
|
|
18
|
+
* apart. Promote to error if that join ever becomes available.
|
|
19
|
+
*
|
|
20
|
+
* Fires only on claims that actually resolved (`problems.length === 0`, via
|
|
21
|
+
* `resolveEncryptedSecretClaims`), not on every raw declaration. An
|
|
22
|
+
* unresolved declaration is already WK8504's job at error severity; firing
|
|
23
|
+
* this warning on top of it would report a Flux wiring gap for a Secret the
|
|
24
|
+
* build has not confirmed exists.
|
|
25
|
+
*/
|
|
26
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
27
|
+
import { allManifests, manifestsOfKind } from "./argo-helpers";
|
|
28
|
+
import type { K8sManifest } from "./k8s-helpers";
|
|
29
|
+
import { resolveEncryptedSecretClaims } from "./sops-helpers";
|
|
30
|
+
|
|
31
|
+
/** Flux Kustomizations only — the kustomize.config.k8s.io kind shares the name. */
|
|
32
|
+
function fluxKustomizations(manifests: K8sManifest[]): K8sManifest[] {
|
|
33
|
+
return manifestsOfKind(manifests, "Kustomization").filter((m) => {
|
|
34
|
+
const apiVersion = m.apiVersion;
|
|
35
|
+
return typeof apiVersion !== "string" || apiVersion.startsWith("kustomize.toolkit.fluxcd.io/");
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const wk8505: PostSynthCheck = {
|
|
40
|
+
id: "WK8505",
|
|
41
|
+
description:
|
|
42
|
+
"committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it",
|
|
43
|
+
|
|
44
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
|
+
const kustomizations = fluxKustomizations(allManifests(ctx));
|
|
46
|
+
if (kustomizations.length === 0) return [];
|
|
47
|
+
if (kustomizations.some((k) => k.spec?.decryption !== undefined)) return [];
|
|
48
|
+
|
|
49
|
+
const resolved = resolveEncryptedSecretClaims(ctx).filter((claim) => claim.problems.length === 0);
|
|
50
|
+
|
|
51
|
+
return resolved.map((claim) => ({
|
|
52
|
+
checkId: "WK8505",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message:
|
|
55
|
+
`declareSecret("${claim.declaration.name}") is committed-encrypted, but no Flux Kustomization ` +
|
|
56
|
+
`in this build sets spec.decryption — Flux will apply "${claim.filename}" without decrypting it. ` +
|
|
57
|
+
`Add decryption: "sops" to the FluxAppFor reconciling the path that carries it, or ignore if that ` +
|
|
58
|
+
`Kustomization is declared in a different build.`,
|
|
59
|
+
entity: claim.declaration.name,
|
|
60
|
+
lexicon: "k8s",
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensureSecret` — the activity behind core's `ensureSecret(...)` op step
|
|
3
|
+
* builder (#1829), provided by the store's lexicon as that builder promises
|
|
4
|
+
* (#1830). Registered by name through the activity registry when a project's
|
|
5
|
+
* `chant.config.ts` lists the `k8s` lexicon, exactly like `kubectlApply`.
|
|
6
|
+
*
|
|
7
|
+
* All it does is connect (the same environment→cluster binding every k8s
|
|
8
|
+
* path uses), build the namespace-scoped store adapter, and run core's one
|
|
9
|
+
* materialization engine over it. The contract — present means done, never
|
|
10
|
+
* mint over an existing value, mismatches fail naming key names only, no
|
|
11
|
+
* output carries material — lives in core (`secret-materialization.ts`) and
|
|
12
|
+
* in the adapter (../../secret-store.ts), not here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ensureSecretMaterialization,
|
|
17
|
+
type EnsureSecretOutcome,
|
|
18
|
+
} from "@intentius/chant/secret-materialization";
|
|
19
|
+
import type { OwnershipMarker } from "@intentius/chant/ownership";
|
|
20
|
+
import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
21
|
+
import { k8sSecretStore } from "../../secret-store";
|
|
22
|
+
|
|
23
|
+
export interface EnsureSecretActivityArgs {
|
|
24
|
+
/** The Secret's `metadata.name`. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** The declared key-set. Creation mints one value per key; verification compares names only. */
|
|
27
|
+
keys: string[];
|
|
28
|
+
/** Declared metadata (labels/annotations) an existing Secret must carry. Mismatches name the KEY. */
|
|
29
|
+
metadata?: Record<string, string>;
|
|
30
|
+
/** Target namespace. Omitted uses the connected context's default namespace. */
|
|
31
|
+
namespace?: string;
|
|
32
|
+
/** chant environment, resolved through `k8s.profiles.<env>.context`. */
|
|
33
|
+
environment?: string;
|
|
34
|
+
/** Explicit kubectl context — same precedence as `kubectlApply`. */
|
|
35
|
+
context?: string;
|
|
36
|
+
/** Project directory whose `chant.config.ts` carries the k8s profiles. */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
/** Ownership stack to stamp on a minted Secret (with `env`, the full marker). */
|
|
39
|
+
stack?: string;
|
|
40
|
+
/** Ownership env to stamp alongside `stack`. */
|
|
41
|
+
env?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ensure a `generated-once` Secret exists in the target cluster. Returns
|
|
46
|
+
* names only — see core's `EnsureSecretOutcome`.
|
|
47
|
+
*/
|
|
48
|
+
export async function ensureSecret(
|
|
49
|
+
args: EnsureSecretActivityArgs,
|
|
50
|
+
_signal?: AbortSignal,
|
|
51
|
+
connect: K8sConnector = defaultK8sConnector,
|
|
52
|
+
): Promise<EnsureSecretOutcome> {
|
|
53
|
+
const { client } = await connect({
|
|
54
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
55
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
56
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const marker: OwnershipMarker | undefined = args.stack
|
|
60
|
+
? { stack: args.stack, ...(args.env ? { env: args.env } : {}) }
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
const store = k8sSecretStore(args.namespace ?? client.defaultNamespace, client, {
|
|
64
|
+
...(marker ? { marker } : {}),
|
|
65
|
+
});
|
|
66
|
+
return ensureSecretMaterialization(store, {
|
|
67
|
+
name: args.name,
|
|
68
|
+
keys: args.keys,
|
|
69
|
+
...(args.metadata ? { metadata: args.metadata } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl";
|
|
21
21
|
export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl";
|
|
22
22
|
|
|
23
|
+
// Generated-once secret materialization (#1830) — the activity behind core's
|
|
24
|
+
// `ensureSecret(...)` step builder (#1829). Read-then-write over the
|
|
25
|
+
// namespace-scoped store adapter in ../../secret-store.ts; no output carries
|
|
26
|
+
// secret material.
|
|
27
|
+
export { ensureSecret } from "./ensure-secret";
|
|
28
|
+
export type { EnsureSecretActivityArgs } from "./ensure-secret";
|
|
29
|
+
|
|
23
30
|
export { waitForArgoSync, defaultArgoStatusFetcher, ArgoSyncFailedError } from "./argo";
|
|
24
31
|
export type { WaitForArgoSyncArgs, ArgoAppStatus, ArgoStatusFetcher } from "./argo";
|
|
25
32
|
|
|
@@ -461,6 +461,38 @@ describe("the ownership-scoped prune (chant #1075)", () => {
|
|
|
461
461
|
expect(result.pruned.map((p) => p.name)).toEqual(["old"]);
|
|
462
462
|
});
|
|
463
463
|
|
|
464
|
+
test("a generated-once secret is excluded from the prunable set and reported retained; a plain owned secret still prunes (#1830)", async () => {
|
|
465
|
+
const cluster = fakeCluster({
|
|
466
|
+
respond: echoApplies,
|
|
467
|
+
objects: {
|
|
468
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: ownedObject("apps/v1", "Deployment", "web", "prod"),
|
|
469
|
+
[objectKey("v1", "Secret", "master-key", "prod")]: ownedObject("v1", "Secret", "master-key", "prod", {
|
|
470
|
+
metadata: {
|
|
471
|
+
labels: {
|
|
472
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
473
|
+
"chant.intentius.io/generated-once": "true",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
[objectKey("v1", "Secret", "stale-config", "prod")]: ownedObject("v1", "Secret", "stale-config", "prod"),
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
const result = await applyManifest(
|
|
481
|
+
{ manifest: manifest(), deleteMode: "owned-only" },
|
|
482
|
+
undefined,
|
|
483
|
+
cluster.connector,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
expect(result.retained).toEqual([
|
|
487
|
+
{ apiVersion: "v1", kind: "Secret", name: "master-key", namespace: "prod" },
|
|
488
|
+
]);
|
|
489
|
+
expect(result.pruned).toEqual([
|
|
490
|
+
{ apiVersion: "v1", kind: "Secret", name: "stale-config", namespace: "prod" },
|
|
491
|
+
]);
|
|
492
|
+
const deletes = cluster.layer.requests.filter((r) => r.method === "DELETE").map((r) => r.path);
|
|
493
|
+
expect(deletes).toEqual(["/api/v1/namespaces/prod/secrets/stale-config"]);
|
|
494
|
+
});
|
|
495
|
+
|
|
464
496
|
test("an object without chant's marker is never a candidate", async () => {
|
|
465
497
|
const cluster = clusterWithOrphans();
|
|
466
498
|
await applyManifest({ manifest: manifest(), deleteMode: "owned-only" }, undefined, cluster.connector);
|
|
@@ -45,6 +45,7 @@ import type { K8sClient, K8sObject } from "@intentius/chant-k8s-client";
|
|
|
45
45
|
import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
|
|
46
46
|
import { operationFor } from "../../api/operation-surface";
|
|
47
47
|
import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types";
|
|
48
|
+
import { isGeneratedOnce } from "../../secret-labels";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* How the apply treats chant-owned objects that are no longer declared. The
|
|
@@ -133,6 +134,15 @@ export interface ApplyManifestResult {
|
|
|
133
134
|
applied: AppliedRef[];
|
|
134
135
|
/** Objects deleted because they carried chant's marker and are no longer declared. */
|
|
135
136
|
pruned: AppliedRef[];
|
|
137
|
+
/**
|
|
138
|
+
* Owned, no longer declared, and deliberately KEPT (#1830, epic #1365
|
|
139
|
+
* decision 5): Secrets carrying the `chant.intentius.io/generated-once`
|
|
140
|
+
* label never enter the prunable set — the stored bytes are the only copy
|
|
141
|
+
* of material chant never held. Reported loudly here and on the console;
|
|
142
|
+
* deletion is an explicit act (`kubectl delete`, or a future gated op),
|
|
143
|
+
* never a prune's. Present whenever the prune ran.
|
|
144
|
+
*/
|
|
145
|
+
retained?: AppliedRef[];
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
/** Read a manifest path — one file, or every YAML/JSON file in a directory. */
|
|
@@ -351,15 +361,15 @@ export async function applyManifest(
|
|
|
351
361
|
}
|
|
352
362
|
|
|
353
363
|
const deleteMode = args.deleteMode ?? "never";
|
|
354
|
-
const pruned =
|
|
364
|
+
const { pruned, retained } =
|
|
355
365
|
deleteMode === "never" || args.dryRun
|
|
356
|
-
? []
|
|
366
|
+
? { pruned: [], retained: [] }
|
|
357
367
|
: await pruneOrphans(client, applied, {
|
|
358
368
|
...(stack !== undefined ? { stack } : {}),
|
|
359
369
|
signal,
|
|
360
370
|
});
|
|
361
371
|
|
|
362
|
-
return { fieldManager, applied, pruned };
|
|
372
|
+
return { fieldManager, applied, pruned, retained };
|
|
363
373
|
} finally {
|
|
364
374
|
clearInterval(heartbeatInterval);
|
|
365
375
|
}
|
|
@@ -407,7 +417,7 @@ async function pruneOrphans(
|
|
|
407
417
|
client: K8sClient,
|
|
408
418
|
applied: readonly AppliedRef[],
|
|
409
419
|
options: { stack?: string; signal?: AbortSignal },
|
|
410
|
-
): Promise<AppliedRef[]> {
|
|
420
|
+
): Promise<{ pruned: AppliedRef[]; retained: AppliedRef[] }> {
|
|
411
421
|
const stack = options.stack;
|
|
412
422
|
const selector = [
|
|
413
423
|
`${LABEL_OWNERSHIP_KEYS.managedBy}=${OWNERSHIP_MANAGED_BY_VALUE}`,
|
|
@@ -476,6 +486,7 @@ async function pruneOrphans(
|
|
|
476
486
|
});
|
|
477
487
|
|
|
478
488
|
const candidates: AppliedRef[] = [];
|
|
489
|
+
const retained: AppliedRef[] = [];
|
|
479
490
|
const seen = new Set<string>();
|
|
480
491
|
for (const { target, item } of found.flat()) {
|
|
481
492
|
const name = item.metadata?.name;
|
|
@@ -494,6 +505,18 @@ async function pruneOrphans(
|
|
|
494
505
|
const key = refKey(ref);
|
|
495
506
|
if (declared.has(key) || seen.has(key)) continue;
|
|
496
507
|
seen.add(key);
|
|
508
|
+
// A generated-once Secret never enters the prunable set (#1830, epic
|
|
509
|
+
// #1365 decision 5): its bytes are the only copy of material chant never
|
|
510
|
+
// held, so no sweep may destroy them. Kept, and said loudly — deletion is
|
|
511
|
+
// an explicit act (`kubectl delete`, or a future gated op).
|
|
512
|
+
if (target.kind === "Secret" && isGeneratedOnce(item.metadata?.labels)) {
|
|
513
|
+
retained.push(ref);
|
|
514
|
+
console.log(
|
|
515
|
+
`${ref.apiVersion} ${ref.kind}/${ref.name} retained (generated-once — owned and no longer declared, ` +
|
|
516
|
+
`but never pruned; delete it explicitly if you mean to)`,
|
|
517
|
+
);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
497
520
|
candidates.push(ref);
|
|
498
521
|
}
|
|
499
522
|
|
|
@@ -504,7 +527,7 @@ async function pruneOrphans(
|
|
|
504
527
|
safeHeartbeat({ step: "prune", pruned: `${ref.kind}/${ref.name}` });
|
|
505
528
|
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
|
|
506
529
|
}
|
|
507
|
-
return pruned;
|
|
530
|
+
return { pruned, retained };
|
|
508
531
|
}
|
|
509
532
|
|
|
510
533
|
function refKey(ref: AppliedRef): string {
|
|
@@ -76,6 +76,40 @@ describe("readiness model", () => {
|
|
|
76
76
|
expect(firstTerminal(ready([{ type: "Ready", status: "False", reason: "AuthenticationFailed" } as never]), git)).toBeDefined();
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
test("CNRM overrides fail fast on Ready=False wedge reasons, DependencyNotReady is not terminal (#1725)", () => {
|
|
80
|
+
// ContainerCluster and ContainerNodePool back examples/cockroachdb-multi-region-gke's
|
|
81
|
+
// `longInfra` waits — a cluster wedged at UpdateFailed used to poll out
|
|
82
|
+
// the whole timeout instead of failing fast.
|
|
83
|
+
const cluster = readinessFor("container.cnrm.cloud.google.com", "ContainerCluster");
|
|
84
|
+
expect(cluster).not.toBe(DEFAULT_READINESS);
|
|
85
|
+
expect(isReady(ready([{ type: "Ready", status: "True" }]), cluster)).toBe(true);
|
|
86
|
+
|
|
87
|
+
const updateFailed = ready([{ type: "Ready", status: "False", reason: "UpdateFailed" } as never]);
|
|
88
|
+
expect(isReady(updateFailed, cluster)).toBe(false);
|
|
89
|
+
expect(firstTerminal(updateFailed, cluster)).toBeDefined();
|
|
90
|
+
|
|
91
|
+
const managementConflict = ready([{ type: "Ready", status: "False", reason: "ManagementConflict" } as never]);
|
|
92
|
+
expect(firstTerminal(managementConflict, cluster)).toBeDefined();
|
|
93
|
+
|
|
94
|
+
// DependencyNotReady is the ordinary "still waiting on another CNRM
|
|
95
|
+
// resource" state (e.g. a cluster waiting on its network) — not a wedge.
|
|
96
|
+
const dependencyNotReady = ready([{ type: "Ready", status: "False", reason: "DependencyNotReady" } as never]);
|
|
97
|
+
expect(firstTerminal(dependencyNotReady, cluster)).toBeUndefined();
|
|
98
|
+
|
|
99
|
+
// In-progress reconciles are not terminal either.
|
|
100
|
+
const updating = ready([{ type: "Ready", status: "False", reason: "Updating" } as never]);
|
|
101
|
+
expect(firstTerminal(updating, cluster)).toBeUndefined();
|
|
102
|
+
|
|
103
|
+
const nodePool = readinessFor("container.cnrm.cloud.google.com", "ContainerNodePool");
|
|
104
|
+
expect(nodePool).not.toBe(DEFAULT_READINESS);
|
|
105
|
+
expect(
|
|
106
|
+
firstTerminal(ready([{ type: "Ready", status: "False", reason: "UpdateFailed" } as never]), nodePool),
|
|
107
|
+
).toBeDefined();
|
|
108
|
+
expect(
|
|
109
|
+
firstTerminal(ready([{ type: "Ready", status: "False", reason: "DependencyNotReady" } as never]), nodePool),
|
|
110
|
+
).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
79
113
|
test("Argo override uses health/sync, not a Ready condition", () => {
|
|
80
114
|
const spec = readinessFor("argoproj.io", "Application");
|
|
81
115
|
expect(spec).not.toBe(DEFAULT_READINESS);
|
|
@@ -98,6 +98,30 @@ const FLUX_TERMINAL = (reasons: string[]): ReadinessSpec => ({
|
|
|
98
98
|
observedGeneration: true,
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Config Connector (CNRM) wedge reasons (#1725): CNRM is kstatus-conformant —
|
|
103
|
+
* `Ready=True` + `observedGeneration` is the correct ready predicate, same as
|
|
104
|
+
* `DEFAULT_READINESS` — so, like `FLUX_TERMINAL`, the value added here is
|
|
105
|
+
* failing fast on a `Ready=False` reason that will not heal by waiting.
|
|
106
|
+
* `UpdateFailed` is CNRM giving up on a reconcile (bad spec, quota, API
|
|
107
|
+
* error); `ManagementConflict` is two controllers fighting over the same GCP
|
|
108
|
+
* resource. Neither resolves on its own.
|
|
109
|
+
*
|
|
110
|
+
* `DependencyNotReady` is deliberately excluded: it is the normal state while
|
|
111
|
+
* a resource waits on another one it depends on (e.g. a `ContainerCluster`
|
|
112
|
+
* waiting on its network) — exactly the "still being created" case this spec
|
|
113
|
+
* has to distinguish from a real wedge, not fold into it.
|
|
114
|
+
*
|
|
115
|
+
* One entry per kind, matching `FLUX_TERMINAL`'s convention, rather than a
|
|
116
|
+
* group-level key: CNRM's kinds don't share a single apiGroup (`ContainerCluster`
|
|
117
|
+
* and `ContainerNodePool` are `container.cnrm.cloud.google.com`; `SQLInstance`
|
|
118
|
+
* would be `sql.cnrm.cloud.google.com`, etc.), so a group-level key would need
|
|
119
|
+
* prefix matching against `*.cnrm.cloud.google.com` — a real change to
|
|
120
|
+
* `readinessFor`'s exact-match lookup for a handful of entries. Not worth it
|
|
121
|
+
* until more CNRM kinds actually need the override.
|
|
122
|
+
*/
|
|
123
|
+
const CNRM_TERMINAL = (): ReadinessSpec => FLUX_TERMINAL(["UpdateFailed", "ManagementConflict"]);
|
|
124
|
+
|
|
101
125
|
export const READINESS_OVERRIDES: Record<string, ReadinessSpec> = {
|
|
102
126
|
"argoproj.io/Application": {
|
|
103
127
|
ready: [
|
|
@@ -139,6 +163,8 @@ export const READINESS_OVERRIDES: Record<string, ReadinessSpec> = {
|
|
|
139
163
|
"ChartPackageError",
|
|
140
164
|
"StorageOperationFailed",
|
|
141
165
|
]),
|
|
166
|
+
"container.cnrm.cloud.google.com/ContainerCluster": CNRM_TERMINAL(),
|
|
167
|
+
"container.cnrm.cloud.google.com/ContainerNodePool": CNRM_TERMINAL(),
|
|
142
168
|
};
|
|
143
169
|
|
|
144
170
|
/** Resolve the readiness spec for a resource: registry override, else default. */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers (chant #1288 Stage 2). Two things asserted:
|
|
3
|
+
*
|
|
4
|
+
* - **Backward compat**: for every input core's original untyped builder
|
|
5
|
+
* accepted, the new typed twin here produces a byte-identical
|
|
6
|
+
* `ActivityStep` (own enumerable fields — `.out` is a non-enumerable
|
|
7
|
+
* getter on both, per `builders.ts`'s `activity()`, so `toEqual` never
|
|
8
|
+
* sees it). Same construction path (`activity()` + `takeProfileAndId`),
|
|
9
|
+
* so this is really asserting the two call sites agree, not re-deriving
|
|
10
|
+
* the logic.
|
|
11
|
+
* - **Step-output refs compose**: a {@link StepOutputRef} in a typed slot
|
|
12
|
+
* type-checks and lands in the step's `args` untouched (chant #1950).
|
|
13
|
+
*
|
|
14
|
+
* The `@ts-expect-error` block at the bottom is a compile-time-only check —
|
|
15
|
+
* never executed, just walked by `npm run typecheck` (tsc), which fails if a
|
|
16
|
+
* marked line does NOT error (an "unused '@ts-expect-error' directive").
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from "vitest";
|
|
20
|
+
import {
|
|
21
|
+
kubectlApply as kubectlApplyOld,
|
|
22
|
+
waitForReady as waitForReadyOld,
|
|
23
|
+
ensureSecret as ensureSecretOld,
|
|
24
|
+
stepOutput,
|
|
25
|
+
type StepOutputRef,
|
|
26
|
+
} from "@intentius/chant/op";
|
|
27
|
+
import { kubectlApply, waitForReady, ensureSecret } from "./builders";
|
|
28
|
+
|
|
29
|
+
describe("k8s typed step builders (#1288 Stage 2)", () => {
|
|
30
|
+
test("kubectlApply: identical ActivityStep to core's original for a minimal call", () => {
|
|
31
|
+
expect(kubectlApply("dist/k8s.yaml")).toEqual(kubectlApplyOld("dist/k8s.yaml"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("kubectlApply: identical ActivityStep with opts (profile override, force, deleteMode)", () => {
|
|
35
|
+
const opts = { context: "kind-dev", force: true, deleteMode: "owned-only" as const, profile: "fastIdempotent" as const };
|
|
36
|
+
expect(kubectlApply("dist/k8s.yaml", opts)).toEqual(kubectlApplyOld("dist/k8s.yaml", opts));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("kubectlApply: accepts a StepOutputRef in a typed slot", () => {
|
|
40
|
+
const ref = stepOutput("build-step", "manifestPath");
|
|
41
|
+
const step = kubectlApply("dist/k8s.yaml", { context: ref });
|
|
42
|
+
expect(step.args?.context).toBe(ref);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("waitForReady: identical ActivityStep to core's original", () => {
|
|
46
|
+
const opts = { namespace: "prod", intervalMs: 5000 };
|
|
47
|
+
expect(waitForReady("certificate", "my-cert", opts)).toEqual(waitForReadyOld("certificate", "my-cert", opts));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("ensureSecret: identical ActivityStep to core's original", () => {
|
|
51
|
+
const opts = { metadata: { team: "platform" } };
|
|
52
|
+
expect(ensureSecret("db-creds", ["password"], opts)).toEqual(ensureSecretOld("db-creds", ["password"], opts));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("kubectlApply: .out is reachable when an id is given", () => {
|
|
56
|
+
const step = kubectlApply("dist/k8s.yaml", { id: "apply" });
|
|
57
|
+
const ref: StepOutputRef = step.out.fieldManager;
|
|
58
|
+
expect(ref.step).toBe("apply");
|
|
59
|
+
expect(ref.path).toBe("fieldManager");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── Compile-time-only: authoring-time type errors (never executed) ──────────
|
|
64
|
+
function _typeChecksOnly(): void {
|
|
65
|
+
// @ts-expect-error — "nameSpace" is not a key of KubectlApplyArgs (the
|
|
66
|
+
// exact typo class chant #1288 names: helmInstall("api", "./chart", {
|
|
67
|
+
// nameSpace: "prod" }) silently no-opped before this).
|
|
68
|
+
kubectlApply("dist/k8s.yaml", { nameSpace: "prod" });
|
|
69
|
+
|
|
70
|
+
// @ts-expect-error — deleteMode must be one of ApplyDeleteMode's literals.
|
|
71
|
+
kubectlApply("dist/k8s.yaml", { deleteMode: "always" });
|
|
72
|
+
|
|
73
|
+
// @ts-expect-error — force must be boolean, not a string.
|
|
74
|
+
kubectlApply("dist/k8s.yaml", { force: "yes" });
|
|
75
|
+
|
|
76
|
+
// @ts-expect-error — waitForReady's readiness spec field is misspelled.
|
|
77
|
+
waitForReady("certificate", "my-cert", { specc: {} });
|
|
78
|
+
|
|
79
|
+
// @ts-expect-error — ensureSecret's keys must be string[], not a single string.
|
|
80
|
+
ensureSecret("db-creds", "password");
|
|
81
|
+
}
|
|
82
|
+
void _typeChecksOnly;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2 — "regenerate the step builders as fully typed wrappers"). Core
|
|
4
|
+
* cannot import a lexicon's `*Args` types (lexicons depend on core, not the
|
|
5
|
+
* other way around — see `packages/core/src/op/activity-contract.ts`'s module
|
|
6
|
+
* doc for the same layering constraint on Stage 1's contracts), so a fully
|
|
7
|
+
* typed `kubectlApply`/`waitForReady`/`ensureSecret` has to live beside the
|
|
8
|
+
* `*Args` interface it derives from: here, next to `./activities/kubectl.ts`
|
|
9
|
+
* etc. `opts`'s type in each wrapper below IS the activity's own `*Args`
|
|
10
|
+
* interface (via `Omit`/`WithStepRefs`) — never a hand-restated mirror — so
|
|
11
|
+
* `kubectlApply`/`waitForReady`/`ensureSecret`'s full option surface (the
|
|
12
|
+
* dozen-plus cross-field options on `kubectlApply` that Stage 1's zod
|
|
13
|
+
* contracts deliberately skipped rather than duplicate) is authoring-time
|
|
14
|
+
* typed for free, and stays in sync with the implementation by construction.
|
|
15
|
+
*
|
|
16
|
+
* Every field also accepts a {@link StepOutputRef} in its place
|
|
17
|
+
* ({@link WithStepRefs}, chant #1950) — a later step's args can consume an
|
|
18
|
+
* earlier step's declared output.
|
|
19
|
+
*
|
|
20
|
+
* `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
|
|
21
|
+
* `@intentius/chant/op`, re-exported from `@intentius/chant-lexicon-temporal`
|
|
22
|
+
* for lexicon-agnostic single-import convenience) are UNCHANGED and produce
|
|
23
|
+
* byte-identical `ActivityStep` output for the same inputs — these are purely
|
|
24
|
+
* additive. Deliberately not swapped into the temporal barrel: that would
|
|
25
|
+
* make `@intentius/chant-lexicon-temporal` depend on this package (and on
|
|
26
|
+
* helm, for `helmInstall`) at runtime, which is exactly the "a worker image
|
|
27
|
+
* needs no kubectl binary" / "temporal stays product-agnostic" property the
|
|
28
|
+
* `op/activities/index.ts` module docs across this repo call out on purpose
|
|
29
|
+
* (activities were moved OUT of temporal into per-product lexicons for this
|
|
30
|
+
* reason; the step-builder layer shouldn't reintroduce the coupling from the
|
|
31
|
+
* other direction). An author who wants the typed surface imports it from
|
|
32
|
+
* here — `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
|
|
33
|
+
* already depends on; existing `@intentius/chant-lexicon-temporal` imports
|
|
34
|
+
* keep working exactly as before, opting in only if the import is changed.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
activity,
|
|
39
|
+
takeProfileAndId,
|
|
40
|
+
type ActivityStep,
|
|
41
|
+
type NamedActivityStep,
|
|
42
|
+
type WithStepRefs,
|
|
43
|
+
} from "@intentius/chant/op";
|
|
44
|
+
import type { KubectlApplyArgs } from "./activities/kubectl";
|
|
45
|
+
import type { WaitForReadyArgs } from "./activities/wait-for-ready";
|
|
46
|
+
import type { EnsureSecretActivityArgs } from "./activities/ensure-secret";
|
|
47
|
+
|
|
48
|
+
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
49
|
+
type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run `kubectl apply -f <manifest>` — the fully typed twin of core's
|
|
53
|
+
* `kubectlApply` step-builder shortcut (chant #1288 Stage 2). `opts` is
|
|
54
|
+
* {@link KubectlApplyArgs} itself (minus the positional `manifest`) — every
|
|
55
|
+
* field `applyManifest` accepts, including `deleteMode`, `force`,
|
|
56
|
+
* `fieldManager`, `documents`, is authoring-time typed here, not just the
|
|
57
|
+
* subset a hand-written zod mirror would bother restating. Defaults to the
|
|
58
|
+
* `longInfra` profile (override via `opts.profile`).
|
|
59
|
+
*/
|
|
60
|
+
export const kubectlApply = (
|
|
61
|
+
manifest: string,
|
|
62
|
+
opts?: WithStepRefs<Omit<KubectlApplyArgs, "manifest">> & StepOpts,
|
|
63
|
+
): NamedActivityStep => {
|
|
64
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
65
|
+
return activity("kubectlApply", { manifest, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Poll a Kubernetes resource until it reports ready, driven by a data-only
|
|
70
|
+
* readiness spec (CRD-aware; #365) — the fully typed twin of core's
|
|
71
|
+
* `waitForReady`. `opts` is {@link WaitForReadyArgs} itself, minus the
|
|
72
|
+
* positional `kind`/`name`. Defaults to the `k8sWait` profile.
|
|
73
|
+
*/
|
|
74
|
+
export const waitForReady = (
|
|
75
|
+
kind: string,
|
|
76
|
+
name: string,
|
|
77
|
+
opts?: WithStepRefs<Omit<WaitForReadyArgs, "kind" | "name">> & StepOpts,
|
|
78
|
+
): NamedActivityStep => {
|
|
79
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
80
|
+
return activity("waitForReady", { kind, name, ...args }, { profile: profile ?? "k8sWait", ...(id ? { id } : {}) });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ensure a `generated-once` secret exists in the target store (#1829) — the
|
|
85
|
+
* fully typed twin of core's `ensureSecret`. `opts` is
|
|
86
|
+
* {@link EnsureSecretActivityArgs} itself, minus the positional `name`/`keys`.
|
|
87
|
+
*/
|
|
88
|
+
export const ensureSecret = (
|
|
89
|
+
name: string,
|
|
90
|
+
keys: string[],
|
|
91
|
+
opts?: WithStepRefs<Omit<EnsureSecretActivityArgs, "name" | "keys">> & StepOpts,
|
|
92
|
+
): NamedActivityStep => {
|
|
93
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
94
|
+
return activity("ensureSecret", { name, keys, ...args }, { profile, ...(id ? { id } : {}) });
|
|
95
|
+
};
|
package/src/plugin.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
|
31
31
|
import { k8sConfigSchema } from "./config-schema";
|
|
32
32
|
import type { K8sChantConfig } from "./config";
|
|
33
33
|
import { renderKustomizeRoots } from "./kustomize/root";
|
|
34
|
+
import { encryptedSecretBuildRoot } from "./sops/encrypted-secret-file";
|
|
35
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
34
36
|
|
|
35
37
|
export const k8sPlugin: LexiconPlugin = {
|
|
36
38
|
name: "k8s",
|
|
@@ -79,10 +81,27 @@ export const k8sPlugin: LexiconPlugin = {
|
|
|
79
81
|
// fallback) and the documents join the build as verbatim manifest entities:
|
|
80
82
|
// serialized with ownership stamping, checked post-synth, observed by
|
|
81
83
|
// `lifecycle diff --live`. See ./kustomize/root.ts.
|
|
84
|
+
//
|
|
85
|
+
// The same hook also resolves committed-encrypted `declareSecret()`
|
|
86
|
+
// declarations: it reads the ciphertext each one names and contributes it
|
|
87
|
+
// as a verbatim sidecar entity (see ./sops/encrypted-secret-file.ts). Both
|
|
88
|
+
// halves are one filesystem read at the one sanctioned impure seam.
|
|
82
89
|
async buildRoots(ctx) {
|
|
90
|
+
const entities = new Map<string, Declarable>();
|
|
91
|
+
const warnings: string[] = [];
|
|
92
|
+
|
|
83
93
|
const roots = (ctx.config as { k8s?: K8sChantConfig }).k8s?.kustomize?.roots ?? [];
|
|
84
|
-
if (roots.length
|
|
85
|
-
|
|
94
|
+
if (roots.length > 0) {
|
|
95
|
+
const rendered = await renderKustomizeRoots({ projectRoot: ctx.projectRoot, roots });
|
|
96
|
+
for (const [name, entity] of rendered.entities) entities.set(name, entity);
|
|
97
|
+
warnings.push(...rendered.warnings);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const encrypted = await encryptedSecretBuildRoot(ctx);
|
|
101
|
+
for (const [name, entity] of encrypted.entities) entities.set(name, entity);
|
|
102
|
+
warnings.push(...(encrypted.warnings ?? []));
|
|
103
|
+
|
|
104
|
+
return { entities, warnings };
|
|
86
105
|
},
|
|
87
106
|
|
|
88
107
|
// K8s YAML has no template interpolation functions like CloudFormation's
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `generated-once` marker label (chant #1830, epic #1365 decision 5).
|
|
3
|
+
*
|
|
4
|
+
* `k8sSecretStore` (./secret-store.ts) stamps this label onto every Secret it
|
|
5
|
+
* mints, next to the ownership marker. Both k8s prune paths key on it:
|
|
6
|
+
*
|
|
7
|
+
* - the apply-path prune (`applyManifest`'s owned-only sweep,
|
|
8
|
+
* op/activities/kubectl.ts) excludes a labeled Secret from the prunable set
|
|
9
|
+
* and reports it `retained` instead of deleting it;
|
|
10
|
+
* - env teardown (./teardown.ts `executeTeardown`) reports the same `retained`
|
|
11
|
+
* outcome for it, loudly, and never deletes it.
|
|
12
|
+
*
|
|
13
|
+
* The rationale is the materialization contract: the stored bytes are the ONLY
|
|
14
|
+
* copy of material chant never held, so no sweep may destroy them. Deletion is
|
|
15
|
+
* an explicit human act — `kubectl delete secret <name>`, or a future gated
|
|
16
|
+
* op — never a side effect of "no longer declared".
|
|
17
|
+
*
|
|
18
|
+
* Dependency-light on purpose: no client import, so the constants are usable
|
|
19
|
+
* from the build path, the prune paths, and the docs/tests alike.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The label key a generated-once Secret carries. */
|
|
23
|
+
export const GENERATED_ONCE_LABEL_KEY = "chant.intentius.io/generated-once";
|
|
24
|
+
|
|
25
|
+
/** The label value `k8sSecretStore` stamps. */
|
|
26
|
+
export const GENERATED_ONCE_LABEL_VALUE = "true";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when a live object's labels mark it generated-once. Any non-empty
|
|
30
|
+
* value counts: the label's presence is the claim, and a sweep must err on
|
|
31
|
+
* the side of keeping.
|
|
32
|
+
*/
|
|
33
|
+
export function isGeneratedOnce(labels: Record<string, unknown> | undefined): boolean {
|
|
34
|
+
const value = labels?.[GENERATED_ONCE_LABEL_KEY];
|
|
35
|
+
return typeof value === "string" && value.length > 0;
|
|
36
|
+
}
|