@intentius/chant-lexicon-k8s 0.124.30 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kubectlApply` — apply a rendered manifest to a cluster.
|
|
3
|
+
*
|
|
4
|
+
* chant #1074 moved this off `kubectl apply -f`. The activity contract is
|
|
5
|
+
* unchanged (a manifest path, an optional context, `Promise<void>`, the
|
|
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
|
+
*
|
|
10
|
+
* chant #1075 finished the job on two axes:
|
|
11
|
+
*
|
|
12
|
+
* - **Identity.** The field manager is no longer the bare `chant` but
|
|
13
|
+
* `chant:<stack>`, derived from the project's `ownership.stack` — the same
|
|
14
|
+
* identity the ownership label already carries. See
|
|
15
|
+
* `@intentius/chant-k8s-client`'s `field-manager.ts`.
|
|
16
|
+
* - **Deletes.** `nativeApply`'s kubectl branch used to shell
|
|
17
|
+
* `kubectl apply --prune --selector <marker>`; that branch now routes here,
|
|
18
|
+
* so the prune had to come with it. {@link applyManifest} implements it
|
|
19
|
+
* against the typed client, scoped to the ownership marker exactly as the
|
|
20
|
+
* shelled prune was, and never touching an object that does not carry it.
|
|
21
|
+
*
|
|
22
|
+
* Consequences worth knowing:
|
|
23
|
+
*
|
|
24
|
+
* - **A worker image needs no `kubectl` binary.** That was the point.
|
|
25
|
+
* - **It is a server-side apply**, rather than the client-side three-way merge
|
|
26
|
+
* `kubectl apply` performs by default. A conflict with another field manager
|
|
27
|
+
* arrives as a `FieldManagerConflictError` naming the competing manager and
|
|
28
|
+
* the contested paths — never as a silent overwrite, and never force-resolved
|
|
29
|
+
* on chant's initiative.
|
|
30
|
+
* - **Documents apply in file order**, as `kubectl apply -f` does, and a
|
|
31
|
+
* directory's files are read in sorted order.
|
|
32
|
+
*/
|
|
33
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { loadAll } from "js-yaml";
|
|
36
|
+
import { loadChantConfig, resolveOwnershipStack } from "@intentius/chant/config";
|
|
37
|
+
import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE, } from "@intentius/chant/ownership";
|
|
38
|
+
import { defaultK8sConnector } from "../../api/connect.js";
|
|
39
|
+
import { operationFor } from "../../api/operation-surface.js";
|
|
40
|
+
import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types.js";
|
|
41
|
+
import { isGeneratedOnce } from "../../secret-labels.js";
|
|
42
|
+
import { isEffectReceiptObject } from "../../effect-receipt-row.js";
|
|
43
|
+
import { diffFields, normalizeServerObject } from "./apply-diff.js";
|
|
44
|
+
/** Read a manifest path — one file, or every YAML/JSON file in a directory. */
|
|
45
|
+
export function readManifestDocuments(path) {
|
|
46
|
+
const files = statSync(path).isDirectory()
|
|
47
|
+
? readdirSync(path)
|
|
48
|
+
.filter((f) => /\.(ya?ml|json)$/i.test(f))
|
|
49
|
+
.sort()
|
|
50
|
+
.map((f) => join(path, f))
|
|
51
|
+
: [path];
|
|
52
|
+
const documents = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
for (const doc of loadAll(readFileSync(file, "utf-8"))) {
|
|
55
|
+
// Multi-document YAML files routinely carry empty documents between
|
|
56
|
+
// separators; they are not objects to apply.
|
|
57
|
+
if (doc && typeof doc === "object")
|
|
58
|
+
documents.push(doc);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return documents;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The field manager to apply as: an explicit one, else `chant:<stack>` from a
|
|
65
|
+
* caller-supplied stack, else `chant:<stack>` from the project's `ownership`
|
|
66
|
+
* config, else the bare `chant`.
|
|
67
|
+
*
|
|
68
|
+
* A project config that cannot be read falls back to the bare `chant` with a
|
|
69
|
+
* warning rather than failing the apply — an activity handed a manifest path
|
|
70
|
+
* and nothing else is a supported way to call this, and its cwd need not be a
|
|
71
|
+
* chant project at all. The warning is there because a *silent* downgrade
|
|
72
|
+
* would change which fields chant owns without saying so.
|
|
73
|
+
*/
|
|
74
|
+
export async function resolveFieldManager(args) {
|
|
75
|
+
return (await resolveApplyIdentity(args)).fieldManager;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The stack and the field manager together, resolved once. The prune needs the
|
|
79
|
+
* stack for its selector and the apply needs the manager; reading the project
|
|
80
|
+
* config twice for one apply would be silly, and could disagree with itself if
|
|
81
|
+
* the file changed in between.
|
|
82
|
+
*/
|
|
83
|
+
async function resolveApplyIdentity(args) {
|
|
84
|
+
const { fieldManagerFor, assertValidFieldManager } = await import("@intentius/chant-k8s-client");
|
|
85
|
+
let stack = args.stack;
|
|
86
|
+
if (stack === undefined) {
|
|
87
|
+
try {
|
|
88
|
+
const { config } = await loadChantConfig(args.cwd ?? process.cwd());
|
|
89
|
+
stack = resolveOwnershipStack(config);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn(`[k8s] could not read the project config to derive a field manager ` +
|
|
93
|
+
`(${err instanceof Error ? err.message : String(err)}); applying as the unqualified "chant"`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (args.fieldManager !== undefined) {
|
|
97
|
+
assertValidFieldManager(args.fieldManager);
|
|
98
|
+
return { fieldManager: args.fieldManager, ...(stack !== undefined ? { stack } : {}) };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
fieldManager: fieldManagerFor(stack !== undefined ? { stack } : undefined),
|
|
102
|
+
...(stack !== undefined ? { stack } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** chant's reserved label channel — the `chant.intentius.io/*` keys the
|
|
106
|
+
* ownership marker lives under, as a conflict-cause field-path prefix. */
|
|
107
|
+
const MARKER_FIELD_PREFIX = `.metadata.labels.${LABEL_OWNERSHIP_KEYS.stack.split("/")[0]}/`;
|
|
108
|
+
/**
|
|
109
|
+
* Is this conflict one chant may retake without a human? Two shapes qualify,
|
|
110
|
+
* per contested field:
|
|
111
|
+
*
|
|
112
|
+
* - the owning manager is chant itself (`chant` bare or `chant:<stack>`) —
|
|
113
|
+
* the ownership-stack → unit-stack migration, or a renamed deploy unit; or
|
|
114
|
+
* - the field lives in chant's OWN reserved label channel
|
|
115
|
+
* (`chant.intentius.io/*`). A controller that reconciles whole objects
|
|
116
|
+
* echoes existing labels and becomes their SSA owner as a side effect
|
|
117
|
+
* (observed live: kubemicrovm's `microvmimagereconciler` owning
|
|
118
|
+
* `.metadata.labels.chant.intentius.io/stack`) — it never chose that value,
|
|
119
|
+
* and chant is the authority for its marker namespace no matter who last
|
|
120
|
+
* echoed it.
|
|
121
|
+
*
|
|
122
|
+
* Any contested field outside both shapes — a spec field, an app label —
|
|
123
|
+
* keeps the presented refusal: that IS a dispute with another tool.
|
|
124
|
+
*/
|
|
125
|
+
function isChantSelfConflict(err) {
|
|
126
|
+
const e = err;
|
|
127
|
+
if (!e || e.name !== "FieldManagerConflictError" || !Array.isArray(e.conflicts) || e.conflicts.length === 0)
|
|
128
|
+
return false;
|
|
129
|
+
return e.conflicts.every((c) => {
|
|
130
|
+
const chantManager = typeof c.manager === "string" && (c.manager === "chant" || c.manager.startsWith("chant:"));
|
|
131
|
+
const markerField = typeof c.field === "string" && c.field.startsWith(MARKER_FIELD_PREFIX);
|
|
132
|
+
return chantManager || markerField;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Stamp the apply's own stack identity onto a document's labels.
|
|
137
|
+
*
|
|
138
|
+
* The serializer bakes `ownership.stack` — the *project* identity from
|
|
139
|
+
* `chant.config.ts` — into every manifest it emits. But the apply's identity
|
|
140
|
+
* is the *stack argument it resolved* (a component's `kubectl-apply` step
|
|
141
|
+
* names its own deploy unit, e.g. `kmv-workload`), and that is the value both
|
|
142
|
+
* consumers of the label query for: the owned-only prune's selector and
|
|
143
|
+
* `describeStackStatus`'s presence sweep. Applying the manifest verbatim left
|
|
144
|
+
* the label saying `kubemicrovm-ops` while both readers asked for
|
|
145
|
+
* `kmv-workload` — the prune silently matched nothing and every kubectl-apply
|
|
146
|
+
* unit read absent in `components status --live` (a green estate painted
|
|
147
|
+
* dead). A manifest chant never serialized (upstream CRDs pinned into a
|
|
148
|
+
* `crds` unit) carried no marker at all, with the same two failures.
|
|
149
|
+
*
|
|
150
|
+
* So the apply now re-stamps what it applies: `managed-by=chant` plus the
|
|
151
|
+
* resolved stack. The ownership *verdict* keys on `managed-by` alone
|
|
152
|
+
* (core/ownership.ts `hasOwnershipMarker`), so overlay classification is
|
|
153
|
+
* unchanged; the env label is the serializer's to write and is left as-is.
|
|
154
|
+
* No stack resolved (no explicit arg, no project ownership) applies verbatim,
|
|
155
|
+
* exactly as before.
|
|
156
|
+
*/
|
|
157
|
+
function stampOwnership(document, stack) {
|
|
158
|
+
if (stack === undefined)
|
|
159
|
+
return document;
|
|
160
|
+
const metadata = (document.metadata ?? {});
|
|
161
|
+
return {
|
|
162
|
+
...document,
|
|
163
|
+
metadata: {
|
|
164
|
+
...metadata,
|
|
165
|
+
labels: {
|
|
166
|
+
...(metadata.labels ?? {}),
|
|
167
|
+
[LABEL_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE,
|
|
168
|
+
[LABEL_OWNERSHIP_KEYS.stack]: stack,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Apply every document in `args.manifest`, then prune when asked.
|
|
175
|
+
*
|
|
176
|
+
* Returns what it did. `kubectlApply` is the registered activity and keeps its
|
|
177
|
+
* `Promise<void>` contract; this is the function `nativeApply` calls, because a
|
|
178
|
+
* dispatcher that reports "applied 12, pruned 2" is worth more than one that
|
|
179
|
+
* reports the shell command it ran.
|
|
180
|
+
*/
|
|
181
|
+
export async function applyManifest(args, signal, connect = defaultK8sConnector) {
|
|
182
|
+
const documents = args.documents ?? readManifestDocuments(args.manifest);
|
|
183
|
+
const { fieldManager, stack } = await resolveApplyIdentity(args);
|
|
184
|
+
const { client } = await connect({
|
|
185
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
186
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
187
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
188
|
+
});
|
|
189
|
+
const applied = [];
|
|
190
|
+
for (const document of documents) {
|
|
191
|
+
const stamped = stampOwnership(document, stack);
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = await client.apply(stamped, {
|
|
195
|
+
fieldManager,
|
|
196
|
+
force: args.force ?? false,
|
|
197
|
+
dryRun: args.dryRun,
|
|
198
|
+
signal,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
// "chant never forces a conflict on its own" is about taking fields
|
|
203
|
+
// from ANOTHER tool. A conflict where every contested field is owned
|
|
204
|
+
// by another `chant:*` manager is chant contesting itself — the
|
|
205
|
+
// ownership-stack → unit-stack label migration, or a renamed deploy
|
|
206
|
+
// unit — and refusing that forever would strand every estate applied
|
|
207
|
+
// before the rename with no non-force path back. Retake those fields
|
|
208
|
+
// deliberately, once, and only when no foreign manager is involved.
|
|
209
|
+
if (!isChantSelfConflict(err))
|
|
210
|
+
throw err;
|
|
211
|
+
result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
|
|
212
|
+
}
|
|
213
|
+
const ref = {
|
|
214
|
+
apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
|
|
215
|
+
kind: String(result.kind ?? document.kind ?? ""),
|
|
216
|
+
name: String(result.metadata?.name ?? ""),
|
|
217
|
+
...(result.metadata?.namespace !== undefined
|
|
218
|
+
? { namespace: String(result.metadata.namespace) }
|
|
219
|
+
: {}),
|
|
220
|
+
};
|
|
221
|
+
applied.push(ref);
|
|
222
|
+
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
|
|
223
|
+
}
|
|
224
|
+
const deleteMode = args.deleteMode ?? "never";
|
|
225
|
+
const { pruned, retained } = deleteMode === "never" || args.dryRun
|
|
226
|
+
? { pruned: [], retained: [] }
|
|
227
|
+
: await pruneOrphans(client, applied, {
|
|
228
|
+
...(stack !== undefined ? { stack } : {}),
|
|
229
|
+
signal,
|
|
230
|
+
});
|
|
231
|
+
return { fieldManager, applied, pruned, retained };
|
|
232
|
+
}
|
|
233
|
+
/** A 404 from the API server, matched by shape. */
|
|
234
|
+
function isNotFound(err) {
|
|
235
|
+
const e = err;
|
|
236
|
+
return !!e && e.name === "K8sApiError" && e.statusCode === 404;
|
|
237
|
+
}
|
|
238
|
+
/** A 422 from the API server whose message says a field is immutable. Matched by shape, as {@link isChantSelfConflict} is. */
|
|
239
|
+
function immutableRefusal(err) {
|
|
240
|
+
const e = err;
|
|
241
|
+
if (!e || e.name !== "K8sApiError" || e.statusCode !== 422)
|
|
242
|
+
return undefined;
|
|
243
|
+
const message = typeof e.apiMessage === "string" && e.apiMessage !== "" ? e.apiMessage : String(e.message ?? "");
|
|
244
|
+
return /immutable/i.test(message) ? message : undefined;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Say what {@link applyManifest} would do, without changing anything (#3998).
|
|
248
|
+
*
|
|
249
|
+
* Every document is stamped and dry-run applied exactly as the apply sends
|
|
250
|
+
* it: the same field manager, `force: false`, and the same retake of a
|
|
251
|
+
* conflict chant has only with itself. The live object is read first and
|
|
252
|
+
* compared with the dry-run result through `./apply-diff.ts`, so a field the
|
|
253
|
+
* server defaults counts as unchanged when the live object already has it.
|
|
254
|
+
*
|
|
255
|
+
* An object in a namespace an earlier document of the same plan creates
|
|
256
|
+
* cannot be dry-run (the server answers 404, as the namespace was never
|
|
257
|
+
* persisted), so it plans as a create of the document as sent.
|
|
258
|
+
*
|
|
259
|
+
* Deletes come from the apply's own prune scope (`findOrphans`): marker
|
|
260
|
+
* scoped, in the namespaces the apply set touches, generated-once Secrets
|
|
261
|
+
* and effect receipts kept. They are found only when `deleteMode` is not
|
|
262
|
+
* `never`, as the apply prunes only then.
|
|
263
|
+
*/
|
|
264
|
+
export async function planManifest(args, signal, connect = defaultK8sConnector) {
|
|
265
|
+
const documents = args.documents ?? readManifestDocuments(args.manifest);
|
|
266
|
+
const { fieldManager, stack } = await resolveApplyIdentity(args);
|
|
267
|
+
const { client } = await connect({
|
|
268
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
269
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
270
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
271
|
+
});
|
|
272
|
+
const objects = [];
|
|
273
|
+
/** Namespaces this plan creates. A dry run cannot place an object in one, since nothing persisted it. */
|
|
274
|
+
const creating = new Set();
|
|
275
|
+
for (const document of documents) {
|
|
276
|
+
const stamped = stampOwnership(document, stack);
|
|
277
|
+
const declaredRef = {
|
|
278
|
+
apiVersion: String(stamped.apiVersion ?? ""),
|
|
279
|
+
kind: String(stamped.kind ?? ""),
|
|
280
|
+
name: String(stamped.metadata?.name ?? ""),
|
|
281
|
+
...(stamped.metadata?.namespace !== undefined ? { namespace: String(stamped.metadata.namespace) } : {}),
|
|
282
|
+
};
|
|
283
|
+
const live = await client.readIfPresent(declaredRef, signal ? { signal } : {});
|
|
284
|
+
let result;
|
|
285
|
+
try {
|
|
286
|
+
try {
|
|
287
|
+
result = await client.apply(stamped, { fieldManager, force: args.force ?? false, dryRun: true, signal });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
if (!isChantSelfConflict(err))
|
|
291
|
+
throw err;
|
|
292
|
+
result = await client.apply(stamped, { fieldManager, force: true, dryRun: true, signal });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (!live && declaredRef.namespace !== undefined && creating.has(declaredRef.namespace) && isNotFound(err)) {
|
|
297
|
+
objects.push({ ref: declaredRef, action: "create", object: stamped, changes: [] });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const refused = live ? immutableRefusal(err) : undefined;
|
|
301
|
+
if (refused === undefined)
|
|
302
|
+
throw err;
|
|
303
|
+
objects.push({ ref: refOf(live, document), action: "update", changes: [], refused });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const ref = refOf(result, document);
|
|
307
|
+
if (!live) {
|
|
308
|
+
if (ref.kind === "Namespace" && ref.apiVersion === "v1")
|
|
309
|
+
creating.add(ref.name);
|
|
310
|
+
objects.push({ ref, action: "create", object: stamped, changes: [] });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const changes = diffFields(normalizeServerObject(live), normalizeServerObject(result));
|
|
314
|
+
objects.push({ ref, action: changes.length === 0 ? "no-op" : "update", changes });
|
|
315
|
+
}
|
|
316
|
+
const deleteMode = args.deleteMode ?? "never";
|
|
317
|
+
const { candidates, retained } = deleteMode === "never"
|
|
318
|
+
? { candidates: [], retained: [] }
|
|
319
|
+
: await findOrphans(client, objects.map((o) => o.ref), { ...(stack !== undefined ? { stack } : {}), ...(signal ? { signal } : {}) });
|
|
320
|
+
return { fieldManager, ...(stack !== undefined ? { stack } : {}), objects, deletes: candidates, retained };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Read each of `refs` as the cluster holds it now, `undefined` for one that
|
|
324
|
+
* does not exist (#3999). `kubectl-apply` reads the fields its step names as
|
|
325
|
+
* outputs from these.
|
|
326
|
+
*/
|
|
327
|
+
export async function readLiveObjects(args, signal, connect = defaultK8sConnector) {
|
|
328
|
+
if (args.refs.length === 0)
|
|
329
|
+
return [];
|
|
330
|
+
const { client } = await connect({
|
|
331
|
+
...(args.environment !== undefined ? { environment: args.environment } : {}),
|
|
332
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
333
|
+
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
|
|
334
|
+
});
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const ref of args.refs)
|
|
337
|
+
out.push((await client.readIfPresent(ref, signal ? { signal } : {})));
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
/** The ref an applied or dry-run object answers with, as {@link applyManifest} records it. */
|
|
341
|
+
function refOf(result, document) {
|
|
342
|
+
return {
|
|
343
|
+
apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
|
|
344
|
+
kind: String(result.kind ?? document.kind ?? ""),
|
|
345
|
+
name: String(result.metadata?.name ?? ""),
|
|
346
|
+
...(result.metadata?.namespace !== undefined ? { namespace: String(result.metadata.namespace) } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Apply every document in `args.manifest`.
|
|
351
|
+
* Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
|
|
352
|
+
*/
|
|
353
|
+
export async function kubectlApply(args, signal, connect = defaultK8sConnector) {
|
|
354
|
+
await applyManifest(args, signal, connect);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Delete chant-owned objects that the apply set no longer declares.
|
|
358
|
+
*
|
|
359
|
+
* This replaces `kubectl apply --prune --selector <marker>=chant`, and keeps
|
|
360
|
+
* its two safety properties exactly:
|
|
361
|
+
*
|
|
362
|
+
* - **Marker-scoped.** Only objects carrying chant's managed-by label are
|
|
363
|
+
* candidates; the label selector is sent to the server, and the marker is
|
|
364
|
+
* re-checked on every object before it is deleted. An unmarked resource is
|
|
365
|
+
* never a candidate, so a hand-made object in the same namespace is safe.
|
|
366
|
+
* - **Namespace-scoped to the apply.** Namespaced kinds are swept only in the
|
|
367
|
+
* namespaces the apply set touched (the context's default namespace when it
|
|
368
|
+
* named none), which is what kubectl's prune does. A manifest that mentions
|
|
369
|
+
* `prod` never reaches into `staging`. Whether a kind *is* namespaced comes
|
|
370
|
+
* from the cluster's own discovery, never from whether a document happened
|
|
371
|
+
* to name a namespace — guessing that wrong is the one way this could widen
|
|
372
|
+
* into a cluster-wide sweep.
|
|
373
|
+
*
|
|
374
|
+
* Which *kinds* get swept is the union of the kinds the apply set contains and
|
|
375
|
+
* chant's default sweep set (`DEFAULT_IMPORT_TYPES` — the same list `chant
|
|
376
|
+
* import` uses when nothing narrows it). The union matters: a kind removed
|
|
377
|
+
* from source entirely appears nowhere in the apply set, and pruning only what
|
|
378
|
+
* is still declared would leave it behind forever. Per-namespace lists rather
|
|
379
|
+
* than one cluster-wide list per kind, so a namespace-scoped service account
|
|
380
|
+
* can still prune.
|
|
381
|
+
*/
|
|
382
|
+
async function pruneOrphans(client, applied, options) {
|
|
383
|
+
const { candidates, retained } = await findOrphans(client, applied, options);
|
|
384
|
+
for (const ref of retained) {
|
|
385
|
+
console.log(ref.kind === "Secret"
|
|
386
|
+
? `${ref.apiVersion} ${ref.kind}/${ref.name} retained (generated-once — owned and no longer declared, ` +
|
|
387
|
+
`but never pruned; delete it explicitly if you mean to)`
|
|
388
|
+
: `${ref.apiVersion} ${ref.kind}/${ref.name} retained (effect receipt: owned and never declared ` +
|
|
389
|
+
`in an apply set, but never pruned; the effect() step is its sole writer)`);
|
|
390
|
+
}
|
|
391
|
+
const pruned = [];
|
|
392
|
+
for (const ref of candidates) {
|
|
393
|
+
await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
|
|
394
|
+
pruned.push(ref);
|
|
395
|
+
console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
|
|
396
|
+
}
|
|
397
|
+
return { pruned, retained };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The objects {@link pruneOrphans} would delete (`candidates`) and the owned,
|
|
401
|
+
* undeclared ones it keeps (`retained`), found without deleting anything. A
|
|
402
|
+
* plan (#3998) asks this for its delete entries, so the plan and the apply
|
|
403
|
+
* share one prune scope.
|
|
404
|
+
*/
|
|
405
|
+
async function findOrphans(client, applied, options) {
|
|
406
|
+
const stack = options.stack;
|
|
407
|
+
const selector = [
|
|
408
|
+
`${LABEL_OWNERSHIP_KEYS.managedBy}=${OWNERSHIP_MANAGED_BY_VALUE}`,
|
|
409
|
+
...(stack ? [`${LABEL_OWNERSHIP_KEYS.stack}=${stack}`] : []),
|
|
410
|
+
].join(",");
|
|
411
|
+
const namespaces = [...new Set(applied.map((a) => a.namespace).filter((n) => !!n))];
|
|
412
|
+
if (namespaces.length === 0 && applied.length > 0)
|
|
413
|
+
namespaces.push(client.defaultNamespace);
|
|
414
|
+
const declared = new Set(applied.map(refKey));
|
|
415
|
+
// Every kind that could hold an orphan: the apply set's own, plus the
|
|
416
|
+
// default sweep set so a kind deleted from source entirely is still reached.
|
|
417
|
+
const sweeps = new Map();
|
|
418
|
+
for (const entityType of DEFAULT_IMPORT_TYPES) {
|
|
419
|
+
const operation = operationFor(entityType);
|
|
420
|
+
if (!operation)
|
|
421
|
+
continue;
|
|
422
|
+
sweeps.set(`${operation.apiVersion}/${operation.kind}`, {
|
|
423
|
+
apiVersion: operation.apiVersion,
|
|
424
|
+
kind: operation.kind,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
for (const ref of applied) {
|
|
428
|
+
if (!ref.apiVersion || !ref.kind)
|
|
429
|
+
continue;
|
|
430
|
+
sweeps.set(`${ref.apiVersion}/${ref.kind}`, { apiVersion: ref.apiVersion, kind: ref.kind });
|
|
431
|
+
}
|
|
432
|
+
// (kind, namespace) pairs to list. Scope comes from the cluster's discovery,
|
|
433
|
+
// which is the only thing that knows it for the version this cluster serves;
|
|
434
|
+
// a kind discovery does not report is dropped rather than swept blindly.
|
|
435
|
+
const resolved = await client.concurrently([...sweeps.values()], async (sweep) => {
|
|
436
|
+
const info = await client
|
|
437
|
+
.resolve({ apiVersion: sweep.apiVersion, kind: sweep.kind }, options.signal)
|
|
438
|
+
.catch(() => undefined);
|
|
439
|
+
return info ? { ...sweep, namespaced: info.namespaced } : undefined;
|
|
440
|
+
});
|
|
441
|
+
const targets = [];
|
|
442
|
+
for (const sweep of resolved) {
|
|
443
|
+
if (!sweep)
|
|
444
|
+
continue;
|
|
445
|
+
if (!sweep.namespaced) {
|
|
446
|
+
targets.push({ apiVersion: sweep.apiVersion, kind: sweep.kind });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
for (const namespace of namespaces) {
|
|
450
|
+
targets.push({ apiVersion: sweep.apiVersion, kind: sweep.kind, namespace });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const found = await client.concurrently(targets, async (target) => {
|
|
454
|
+
try {
|
|
455
|
+
const items = await client.list({ apiVersion: target.apiVersion, kind: target.kind }, {
|
|
456
|
+
labelSelector: selector,
|
|
457
|
+
...(target.namespace !== undefined ? { namespace: target.namespace } : {}),
|
|
458
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
459
|
+
});
|
|
460
|
+
return items.map((item) => ({ target, item }));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Kind not served here, or the read was denied. Neither is evidence that
|
|
464
|
+
// something should be deleted, so the sweep skips it rather than failing
|
|
465
|
+
// the apply that already succeeded.
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
const candidates = [];
|
|
470
|
+
const retained = [];
|
|
471
|
+
const seen = new Set();
|
|
472
|
+
for (const { target, item } of found.flat()) {
|
|
473
|
+
const name = item.metadata?.name;
|
|
474
|
+
if (!name)
|
|
475
|
+
continue;
|
|
476
|
+
// The server filtered by label already; this re-check is the one that
|
|
477
|
+
// matters, because a delete is not undoable.
|
|
478
|
+
if (!hasOwnershipMarker(item.metadata?.labels, LABEL_OWNERSHIP_KEYS))
|
|
479
|
+
continue;
|
|
480
|
+
// Already terminating — deleting again is noise, not progress.
|
|
481
|
+
if (item.metadata?.deletionTimestamp !== undefined)
|
|
482
|
+
continue;
|
|
483
|
+
const ref = {
|
|
484
|
+
apiVersion: target.apiVersion,
|
|
485
|
+
kind: target.kind,
|
|
486
|
+
name,
|
|
487
|
+
...(item.metadata?.namespace !== undefined ? { namespace: item.metadata.namespace } : {}),
|
|
488
|
+
};
|
|
489
|
+
const key = refKey(ref);
|
|
490
|
+
if (declared.has(key) || seen.has(key))
|
|
491
|
+
continue;
|
|
492
|
+
seen.add(key);
|
|
493
|
+
// A generated-once Secret never enters the prunable set (#1830, epic
|
|
494
|
+
// #1365 decision 5): its bytes are the only copy of material chant never
|
|
495
|
+
// held, so no sweep may destroy them. Kept, and said loudly — deletion is
|
|
496
|
+
// an explicit act (`kubectl delete`, or a future gated op).
|
|
497
|
+
if (target.kind === "Secret" && isGeneratedOnce(item.metadata?.labels)) {
|
|
498
|
+
retained.push(ref);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
// An effect receipt never enters the prunable set either (#2074, epic
|
|
502
|
+
// #1703 decision 3). It is chant-owned and is never in ANY apply set,
|
|
503
|
+
// the `effect()` step being its sole writer, which is exactly the shape
|
|
504
|
+
// this sweep deletes. Pruning one would tell the next plan that an effect
|
|
505
|
+
// never ran and re-fire it, or worse, be re-stamped by nothing and leave
|
|
506
|
+
// the estate re-running a migration on every apply.
|
|
507
|
+
if (target.kind === "ConfigMap" && isEffectReceiptObject(item.metadata?.labels)) {
|
|
508
|
+
retained.push(ref);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
candidates.push(ref);
|
|
512
|
+
}
|
|
513
|
+
return { candidates, retained };
|
|
514
|
+
}
|
|
515
|
+
function refKey(ref) {
|
|
516
|
+
return `${ref.apiVersion}/${ref.kind}/${ref.namespace ?? ""}/${ref.name}`;
|
|
517
|
+
}
|