@intentius/chant-lexicon-k8s 0.124.31 → 0.124.33
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,51 @@
|
|
|
1
|
+
/** True when the ref is an Argo CD Application (any argoproj.io version). */
|
|
2
|
+
function isArgoApplication(ref) {
|
|
3
|
+
return ref.kind === "Application" && /^argoproj\.io(\/|$)/.test(ref.apiVersion);
|
|
4
|
+
}
|
|
5
|
+
/** Factory with injectable applier + waiter, so tests assert the exact
|
|
6
|
+
* delegation without a cluster — the kubectl-apply seam. Defaults resolve by
|
|
7
|
+
* dynamic import on first run, keeping the API-client chain off the build
|
|
8
|
+
* path (#1074). */
|
|
9
|
+
export function createArgoAppCapability(apply, wait) {
|
|
10
|
+
return {
|
|
11
|
+
kind: "argo-app",
|
|
12
|
+
// The apply half is a server-side apply with no native undo, and Argo
|
|
13
|
+
// offers no declarative "un-sync" — same posture as kubectl-apply.
|
|
14
|
+
rollbackPolicy: "needs-opt-out",
|
|
15
|
+
async run(ctx, input) {
|
|
16
|
+
const applier = apply ?? (await import("../op/activities/kubectl.js")).applyManifest;
|
|
17
|
+
const outcome = await applier({
|
|
18
|
+
manifest: input.manifest,
|
|
19
|
+
environment: ctx.env,
|
|
20
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
21
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
22
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
23
|
+
});
|
|
24
|
+
// AppProject / ApplicationSet in the same manifest apply fine; the wait
|
|
25
|
+
// gates only on Applications (an ApplicationSet's generated Applications
|
|
26
|
+
// are the controller's, not declared here).
|
|
27
|
+
const apps = outcome.applied.filter(isArgoApplication);
|
|
28
|
+
if (apps.length === 0) {
|
|
29
|
+
throw new Error(`argo-app: "${input.manifest}" applied no argoproj.io Application — nothing to sync on. ` +
|
|
30
|
+
`A plain manifest belongs on kubectl-apply.`);
|
|
31
|
+
}
|
|
32
|
+
const waiter = wait ?? (await import("../op/activities/argo.js")).waitForArgoSync;
|
|
33
|
+
const synced = [];
|
|
34
|
+
for (const app of apps) {
|
|
35
|
+
const namespace = app.namespace ?? "argocd";
|
|
36
|
+
const status = await waiter({
|
|
37
|
+
appName: app.name,
|
|
38
|
+
namespace,
|
|
39
|
+
...(input.server !== undefined ? { server: input.server } : {}),
|
|
40
|
+
...(input.authToken !== undefined ? { authToken: input.authToken } : {}),
|
|
41
|
+
...(input.insecure !== undefined ? { insecure: input.insecure } : {}),
|
|
42
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
43
|
+
...(input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {}),
|
|
44
|
+
});
|
|
45
|
+
synced.push({ name: app.name, namespace, health: status.health, sync: status.sync });
|
|
46
|
+
}
|
|
47
|
+
return { ...outcome, synced };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export const argoAppCapability = createArgoAppCapability();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builders for the k8s lexicon's verbs — the same ergonomic sugar
|
|
3
|
+
* the aws lexicon's `components/builders.ts` offers (#658), reusing the
|
|
4
|
+
* exported `step` projection from `@intentius/chant/components`.
|
|
5
|
+
*
|
|
6
|
+
* `kubectl-apply` is a `needs-opt-out` capability (a server-side apply keeps
|
|
7
|
+
* no previous object state, so there is nothing native to roll back to), which
|
|
8
|
+
* means COMP003 requires every step of it to carry a `noRollback: "<reason>"`,
|
|
9
|
+
* a component-level `rollback` phase, or a sibling safety step. The builder
|
|
10
|
+
* admits `noRollback` directly so the common opt-out reads as one typed call
|
|
11
|
+
* instead of a raw object literal.
|
|
12
|
+
*/
|
|
13
|
+
import { step } from "@intentius/chant/components";
|
|
14
|
+
export const kubectlApply = step("kubectl-apply");
|
|
15
|
+
// Same needs-opt-out posture, same sugar (#1548): a kustomize render in front
|
|
16
|
+
// of the identical server-side apply.
|
|
17
|
+
export const kustomizeApply = step("kustomize-apply");
|
|
18
|
+
// GitOps verbs (#1549 piece 2): the identical server-side apply of the
|
|
19
|
+
// controller's CR, then a convergence wait — Healthy+Synced for Argo, the
|
|
20
|
+
// Flux readiness registry (Ready, terminal on wedge reasons) for Flux.
|
|
21
|
+
export const argoApp = step("argo-app");
|
|
22
|
+
export const fluxReconcile = step("flux-reconcile");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ownPackageVersion } from "@intentius/chant/components/capability-plugin";
|
|
2
|
+
import { kubectlApplyCapability } from "./kubectl-apply.js";
|
|
3
|
+
import { kustomizeApplyCapability } from "./kustomize-apply.js";
|
|
4
|
+
import { argoAppCapability } from "./argo-app.js";
|
|
5
|
+
import { fluxReconcileCapability } from "./flux-reconcile.js";
|
|
6
|
+
export const K8S_VERB_FAMILIES = {
|
|
7
|
+
apply: ["kubectl-apply", "kustomize-apply"],
|
|
8
|
+
// GitOps verbs (#1549 piece 2): apply the controller's CR through the same
|
|
9
|
+
// stack-labelled pipeline, then wait for the controller to converge.
|
|
10
|
+
gitops: ["argo-app", "flux-reconcile"],
|
|
11
|
+
};
|
|
12
|
+
export const k8sCapabilityPlugin = {
|
|
13
|
+
name: "k8s",
|
|
14
|
+
// The lexicon package's own version (#1505) — lockstep releases bump it, so
|
|
15
|
+
// a literal here was stale one release after it was written. A getter, so
|
|
16
|
+
// the package.json read happens on first access rather than at import time.
|
|
17
|
+
get version() {
|
|
18
|
+
return ownPackageVersion(import.meta.url);
|
|
19
|
+
},
|
|
20
|
+
capabilities() {
|
|
21
|
+
return [
|
|
22
|
+
kubectlApplyCapability,
|
|
23
|
+
kustomizeApplyCapability,
|
|
24
|
+
argoAppCapability,
|
|
25
|
+
fluxReconcileCapability,
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
families() {
|
|
29
|
+
return K8S_VERB_FAMILIES;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** The API group of a ref, "" for the core group. */
|
|
2
|
+
function groupOf(ref) {
|
|
3
|
+
const slash = ref.apiVersion.indexOf("/");
|
|
4
|
+
return slash === -1 ? "" : ref.apiVersion.slice(0, slash);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* True for any Flux CR: the toolkit groups (`source.` / `kustomize.` /
|
|
8
|
+
* `helm.` / `image.` / `notification.toolkit.fluxcd.io`) plus the flux-operator
|
|
9
|
+
* group (`fluxcd.controlplane.io`, `FluxInstance`). Kinds without a registry
|
|
10
|
+
* entry still wait correctly — the toolkit is kstatus-conformant, so the
|
|
11
|
+
* generic `Ready=True` default covers them; the #1554 entries add fail-fast.
|
|
12
|
+
*/
|
|
13
|
+
function isFluxRef(ref) {
|
|
14
|
+
const group = groupOf(ref);
|
|
15
|
+
return /(^|\.)toolkit\.fluxcd\.io$/.test(group) || group === "fluxcd.controlplane.io";
|
|
16
|
+
}
|
|
17
|
+
/** Factory with injectable applier + waiter — the argo-app seam. Defaults
|
|
18
|
+
* resolve by dynamic import on first run (#1074). */
|
|
19
|
+
export function createFluxReconcileCapability(apply, wait) {
|
|
20
|
+
return {
|
|
21
|
+
kind: "flux-reconcile",
|
|
22
|
+
// Server-side apply keeps no previous object state, and the controller
|
|
23
|
+
// reconciles forward only — same posture as kubectl-apply.
|
|
24
|
+
rollbackPolicy: "needs-opt-out",
|
|
25
|
+
async run(ctx, input) {
|
|
26
|
+
const applier = apply ?? (await import("../op/activities/kubectl.js")).applyManifest;
|
|
27
|
+
const outcome = await applier({
|
|
28
|
+
manifest: input.manifest,
|
|
29
|
+
environment: ctx.env,
|
|
30
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
31
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
32
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
33
|
+
});
|
|
34
|
+
// Namespaces/ConfigMaps alongside the CRs apply fine; the wait gates
|
|
35
|
+
// only on the Flux objects. Sources first — see the module doc.
|
|
36
|
+
const flux = outcome.applied.filter(isFluxRef);
|
|
37
|
+
if (flux.length === 0) {
|
|
38
|
+
throw new Error(`flux-reconcile: "${input.manifest}" applied no Flux CR (toolkit.fluxcd.io / fluxcd.controlplane.io) — ` +
|
|
39
|
+
`nothing to reconcile on. A plain manifest belongs on kubectl-apply.`);
|
|
40
|
+
}
|
|
41
|
+
const sources = flux.filter((r) => groupOf(r) === "source.toolkit.fluxcd.io");
|
|
42
|
+
const rest = flux.filter((r) => groupOf(r) !== "source.toolkit.fluxcd.io");
|
|
43
|
+
const waiter = wait ?? (await import("../op/activities/wait-for-ready.js")).waitForReady;
|
|
44
|
+
const ready = [];
|
|
45
|
+
for (const ref of [...sources, ...rest]) {
|
|
46
|
+
await waiter({
|
|
47
|
+
kind: ref.kind,
|
|
48
|
+
name: ref.name,
|
|
49
|
+
group: groupOf(ref),
|
|
50
|
+
environment: ctx.env,
|
|
51
|
+
...(ref.namespace !== undefined ? { namespace: ref.namespace } : {}),
|
|
52
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
53
|
+
...(input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {}),
|
|
54
|
+
});
|
|
55
|
+
ready.push(ref);
|
|
56
|
+
}
|
|
57
|
+
return { ...outcome, ready };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export const fluxReconcileCapability = createFluxReconcileCapability();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin.js";
|
|
14
14
|
export * from "./builders.js";
|
|
15
|
-
export { kubectlApplyCapability, createKubectlApplyCapability, type KubectlApplyInput, type KubectlApplyOutcome, } from "./kubectl-apply.js";
|
|
15
|
+
export { kubectlApplyCapability, createKubectlApplyCapability, KubectlApplyPlanMovedError, type KubectlApplyInput, type KubectlApplyOutcome, type KubectlApplyDryRun, type KubectlApplyPlanArtifact, } from "./kubectl-apply.js";
|
|
16
16
|
export { kustomizeApplyCapability, createKustomizeApplyCapability, renderCommand, type KustomizeApplyInput, type KustomizeApplyOutcome, } from "./kustomize-apply.js";
|
|
17
17
|
export { argoAppCapability, createArgoAppCapability, type ArgoAppInput, type ArgoAppOutcome, type ArgoAppSynced, } from "./argo-app.js";
|
|
18
18
|
export { fluxReconcileCapability, createFluxReconcileCapability, type FluxReconcileInput, type FluxReconcileOutcome, } from "./flux-reconcile.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,0BAA0B,EAC1B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,8BAA8B,EAC9B,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s lexicon's component/release surface: the `kubectl-apply` capability,
|
|
3
|
+
* its typed step-builder, and the `k8sCapabilityPlugin` core loads when a
|
|
4
|
+
* project declares `lexicons: ["k8s"]` (#1495). Component authors import the
|
|
5
|
+
* builders from here (`@intentius/chant-lexicon-k8s/components`), the way AWS
|
|
6
|
+
* verbs come from `@intentius/chant-lexicon-aws/components`.
|
|
7
|
+
*
|
|
8
|
+
* Everything exported here is on the build path (#1074): nothing may
|
|
9
|
+
* statically import the Kubernetes API client chain (`../op/activities/*`,
|
|
10
|
+
* `../kube/*`) — `kubectl-apply.ts` reaches its applier by dynamic import
|
|
11
|
+
* inside `run()`, and this module must stay that clean.
|
|
12
|
+
*/
|
|
13
|
+
export { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin.js";
|
|
14
|
+
export * from "./builders.js";
|
|
15
|
+
export { kubectlApplyCapability, createKubectlApplyCapability, KubectlApplyPlanMovedError, } from "./kubectl-apply.js";
|
|
16
|
+
export { kustomizeApplyCapability, createKustomizeApplyCapability, renderCommand, } from "./kustomize-apply.js";
|
|
17
|
+
export { argoAppCapability, createArgoAppCapability, } from "./argo-app.js";
|
|
18
|
+
export { fluxReconcileCapability, createFluxReconcileCapability, } from "./flux-reconcile.js";
|
|
@@ -14,6 +14,18 @@
|
|
|
14
14
|
* (`chant:<stack>`, the prune selector) and the deploy unit the status walk
|
|
15
15
|
* reads (core `components/deploy-units.ts`) — one name, both jobs, the same
|
|
16
16
|
* pairing `cfn-deploy` has with its stack.
|
|
17
|
+
*
|
|
18
|
+
* ## Outputs (#3999)
|
|
19
|
+
*
|
|
20
|
+
* A manifest has no outputs of its own, so a step names the ones a
|
|
21
|
+
* dependent reads: `outputs: { host: { object: "Service/prod/web", field:
|
|
22
|
+
* "status.loadBalancer.ingress.0.hostname" } }`, each a field of an object
|
|
23
|
+
* the manifest declares. `run` returns them as `outputs` once it applied,
|
|
24
|
+
* which is what a dependent's `stackOutput()` reads, and `outputs` reads
|
|
25
|
+
* them from the cluster without applying anything. A plan knows a field the
|
|
26
|
+
* manifest sets, since the apply writes that value, and any field of an
|
|
27
|
+
* object the plan leaves as it is; every other one is known only after
|
|
28
|
+
* apply.
|
|
17
29
|
*/
|
|
18
30
|
import type { Capability } from "@intentius/chant/components/capability";
|
|
19
31
|
/**
|
|
@@ -45,6 +57,8 @@ export interface KubectlApplyOutcome {
|
|
|
45
57
|
* enter the prunable set. Present whenever the prune ran.
|
|
46
58
|
*/
|
|
47
59
|
retained?: AppliedRef[];
|
|
60
|
+
/** The fields the step names as outputs (#3999), read once the apply landed. Absent when it names none. */
|
|
61
|
+
outputs?: Record<string, unknown>;
|
|
48
62
|
}
|
|
49
63
|
interface ApplierArgs {
|
|
50
64
|
manifest: string;
|
|
@@ -57,6 +71,59 @@ interface ApplierArgs {
|
|
|
57
71
|
}
|
|
58
72
|
type Applier = (args: ApplierArgs) => Promise<KubectlApplyOutcome>;
|
|
59
73
|
type ManifestReader = (path: string) => Record<string, unknown>[];
|
|
74
|
+
/** One field change, the mirror of `../op/activities/apply-diff.ts`'s `FieldChange`. */
|
|
75
|
+
interface FieldChangeShape {
|
|
76
|
+
path: string;
|
|
77
|
+
before?: unknown;
|
|
78
|
+
after?: unknown;
|
|
79
|
+
}
|
|
80
|
+
/** The mirror of the activity's `PlannedObject`. */
|
|
81
|
+
interface PlannedObjectShape {
|
|
82
|
+
ref: AppliedRef;
|
|
83
|
+
action: "create" | "update" | "no-op";
|
|
84
|
+
object?: Record<string, unknown>;
|
|
85
|
+
changes: FieldChangeShape[];
|
|
86
|
+
refused?: string;
|
|
87
|
+
}
|
|
88
|
+
/** What a dry run found: the mirror of the activity's `ManifestPlanResult` (#3998). */
|
|
89
|
+
export interface KubectlApplyDryRun {
|
|
90
|
+
fieldManager: string;
|
|
91
|
+
stack?: string;
|
|
92
|
+
objects: PlannedObjectShape[];
|
|
93
|
+
deletes: AppliedRef[];
|
|
94
|
+
retained: AppliedRef[];
|
|
95
|
+
}
|
|
96
|
+
type Planner = (args: ApplierArgs) => Promise<KubectlApplyDryRun>;
|
|
97
|
+
/** The mirror of the activity's `ReadLiveObjectsArgs`. */
|
|
98
|
+
interface LiveReaderArgs {
|
|
99
|
+
refs: AppliedRef[];
|
|
100
|
+
environment?: string;
|
|
101
|
+
context?: string;
|
|
102
|
+
}
|
|
103
|
+
/** Reads objects as the cluster holds them, `undefined` for one that does not exist (#3999). */
|
|
104
|
+
type LiveReader = (args: LiveReaderArgs) => Promise<Array<Record<string, unknown> | undefined>>;
|
|
105
|
+
/** What `plan` hands `run` through `DeployContext.plans` (#3998). */
|
|
106
|
+
export interface KubectlApplyPlanArtifact {
|
|
107
|
+
kind: "kubectl-apply-plan";
|
|
108
|
+
planDigest: string;
|
|
109
|
+
/** The documents the plan dry-ran, release attributes set. `run` applies these. */
|
|
110
|
+
documents: Record<string, unknown>[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `run` was handed an approved plan and a fresh dry run of the same
|
|
114
|
+
* documents gives a different digest: the cluster moved since the plan, or
|
|
115
|
+
* the plan was made against another one. Nothing is applied. `planMoved` is
|
|
116
|
+
* core's mark for a moved plan (`isPlanMovedError`), so the command that
|
|
117
|
+
* applied it exits 3. `exitCode` 3 says the same to a core that reads it.
|
|
118
|
+
*/
|
|
119
|
+
export declare class KubectlApplyPlanMovedError extends Error {
|
|
120
|
+
readonly member: string;
|
|
121
|
+
readonly approved: string;
|
|
122
|
+
readonly fresh: string;
|
|
123
|
+
readonly planMoved = true;
|
|
124
|
+
readonly exitCode = 3;
|
|
125
|
+
constructor(member: string, approved: string, fresh: string);
|
|
126
|
+
}
|
|
60
127
|
export interface KubectlApplyInput {
|
|
61
128
|
/** Path to a manifest file, or a directory of them. */
|
|
62
129
|
manifest: string;
|
|
@@ -76,12 +143,27 @@ export interface KubectlApplyInput {
|
|
|
76
143
|
* candidate.
|
|
77
144
|
*/
|
|
78
145
|
delete?: "never" | "owned-only";
|
|
146
|
+
/**
|
|
147
|
+
* The outputs the step exposes (#3999), by name: each a field of an object
|
|
148
|
+
* the manifest declares. A dependent reads them with `stackOutput()`.
|
|
149
|
+
*/
|
|
150
|
+
outputs?: Record<string, KubectlApplyOutputField>;
|
|
151
|
+
}
|
|
152
|
+
/** One output of a `kubectl-apply` step (#3999). */
|
|
153
|
+
export interface KubectlApplyOutputField {
|
|
154
|
+
/**
|
|
155
|
+
* The object, named as the manifest declares it: `Kind/namespace/name`, or
|
|
156
|
+
* `Kind/name` for a document with no namespace.
|
|
157
|
+
*/
|
|
158
|
+
object: string;
|
|
159
|
+
/** The field's dot path in the object; a list index is a number, `status.loadBalancer.ingress.0.hostname`. */
|
|
160
|
+
field: string;
|
|
79
161
|
}
|
|
80
162
|
/** Factory with an injectable applier, so tests assert the delegation without
|
|
81
163
|
* a cluster — the same seam `createGenerateSbomCapability` uses. The default
|
|
82
164
|
* applier is resolved by dynamic import on first run, keeping the API-client
|
|
83
165
|
* chain off the build path (#1074). */
|
|
84
|
-
export declare function createKubectlApplyCapability(apply?: Applier, read?: ManifestReader): Capability<KubectlApplyInput, KubectlApplyOutcome>;
|
|
166
|
+
export declare function createKubectlApplyCapability(apply?: Applier, read?: ManifestReader, dryRun?: Planner, readLive?: LiveReader): Capability<KubectlApplyInput, KubectlApplyOutcome>;
|
|
85
167
|
export declare const kubectlApplyCapability: Capability<KubectlApplyInput, KubectlApplyOutcome>;
|
|
86
168
|
export {};
|
|
87
169
|
//# sourceMappingURL=kubectl-apply.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kubectl-apply.d.ts","sourceRoot":"","sources":["../../src/components/kubectl-apply.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"kubectl-apply.d.ts","sourceRoot":"","sources":["../../src/components/kubectl-apply.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
2
|
+
import { releaseAttributesSuffix } from "@intentius/chant/telemetry-attribution";
|
|
3
|
+
import { annotateRelease } from "../telemetry-release.js";
|
|
4
|
+
import { diffFields, normalizeServerObject } from "../op/activities/apply-diff.js";
|
|
5
|
+
function isPlanArtifact(value) {
|
|
6
|
+
const v = value;
|
|
7
|
+
return !!v && v.kind === "kubectl-apply-plan" && typeof v.planDigest === "string" && Array.isArray(v.documents);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `run` was handed an approved plan and a fresh dry run of the same
|
|
11
|
+
* documents gives a different digest: the cluster moved since the plan, or
|
|
12
|
+
* the plan was made against another one. Nothing is applied. `planMoved` is
|
|
13
|
+
* core's mark for a moved plan (`isPlanMovedError`), so the command that
|
|
14
|
+
* applied it exits 3. `exitCode` 3 says the same to a core that reads it.
|
|
15
|
+
*/
|
|
16
|
+
export class KubectlApplyPlanMovedError extends Error {
|
|
17
|
+
constructor(member, approved, fresh) {
|
|
18
|
+
super(`kubectl-apply "${member}": the plan moved since it was approved (approved ${approved}, now ${fresh}); ` +
|
|
19
|
+
`nothing was applied. Plan and approve again.`);
|
|
20
|
+
this.member = member;
|
|
21
|
+
this.approved = approved;
|
|
22
|
+
this.fresh = fresh;
|
|
23
|
+
this.planMoved = true;
|
|
24
|
+
this.exitCode = 3;
|
|
25
|
+
this.name = "KubectlApplyPlanMovedError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Kinds whose pod template change rolls their pods. */
|
|
29
|
+
const ROLLING_KINDS = new Set(["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"]);
|
|
30
|
+
/** `Deployment/prod/web`, or `ClusterRole/admin` for a cluster-scoped object. */
|
|
31
|
+
function addressOf(ref) {
|
|
32
|
+
return ref.namespace ? `${ref.kind}/${ref.namespace}/${ref.name}` : `${ref.kind}/${ref.name}`;
|
|
33
|
+
}
|
|
34
|
+
const typeOf = (ref) => `${ref.apiVersion}/${ref.kind}`;
|
|
35
|
+
/** A Secret's `data` and `stringData` values are never carried into the change set. */
|
|
36
|
+
function isSensitive(ref, path) {
|
|
37
|
+
return ref.kind === "Secret" && /^(data|stringData)\b/.test(path);
|
|
38
|
+
}
|
|
39
|
+
function attribute(ref, change) {
|
|
40
|
+
if (isSensitive(ref, change.path))
|
|
41
|
+
return { path: change.path, sensitive: true };
|
|
42
|
+
return {
|
|
43
|
+
path: change.path,
|
|
44
|
+
...("before" in change ? { before: change.before } : {}),
|
|
45
|
+
...("after" in change ? { after: change.after } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The member's change-set part: one entry per declared object (`no-op`
|
|
50
|
+
* included, as the terraform adapter reports them) and one `delete` per
|
|
51
|
+
* object the prune would remove. A change the API server refuses as
|
|
52
|
+
* immutable is an `update` with disruption `unknown`, plus a hole that says
|
|
53
|
+
* the apply would fail.
|
|
54
|
+
*/
|
|
55
|
+
function changeSetPartOf(member, planDigest, dryRun) {
|
|
56
|
+
const base = { member, lexicon: "k8s", planner: "chant" };
|
|
57
|
+
const entries = [];
|
|
58
|
+
const holes = [];
|
|
59
|
+
for (const o of dryRun.objects) {
|
|
60
|
+
const head = { ...base, address: addressOf(o.ref), type: typeOf(o.ref), name: o.ref.name };
|
|
61
|
+
if (o.refused !== undefined) {
|
|
62
|
+
entries.push({ ...head, action: "update", disruption: "unknown", attributes: [] });
|
|
63
|
+
holes.push({
|
|
64
|
+
address: head.address,
|
|
65
|
+
type: head.type,
|
|
66
|
+
reason: `the API server refuses the change: ${o.refused}. The apply does not delete and recreate ` +
|
|
67
|
+
`the object, so it fails the same way until the object is deleted`,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (o.action === "create") {
|
|
72
|
+
const declared = normalizeServerObject(o.object ?? {});
|
|
73
|
+
delete declared.apiVersion;
|
|
74
|
+
delete declared.kind;
|
|
75
|
+
entries.push({ ...head, action: "create", attributes: diffFields({}, declared).map((c) => attribute(o.ref, c)) });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (o.action === "no-op") {
|
|
79
|
+
entries.push({ ...head, action: "no-op", attributes: [] });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const rolling = ROLLING_KINDS.has(o.ref.kind) && o.changes.some((c) => c.path.startsWith("spec.template"));
|
|
83
|
+
entries.push({
|
|
84
|
+
...head,
|
|
85
|
+
action: "update",
|
|
86
|
+
disruption: rolling ? "rolling" : "in-place",
|
|
87
|
+
attributes: o.changes.map((c) => attribute(o.ref, c)),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const ref of dryRun.deletes) {
|
|
91
|
+
entries.push({ ...base, address: addressOf(ref), type: typeOf(ref), name: ref.name, action: "delete", disruption: "destroy", attributes: [] });
|
|
92
|
+
}
|
|
93
|
+
return { member: { ...base, status: "planned", planDigest, holes }, entries };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What a gate on this member binds: where it applies, as whom, the documents
|
|
97
|
+
* sent, and what the dry run says each does. For an object it changes, the
|
|
98
|
+
* changed paths and their new values, so a field another manager moves
|
|
99
|
+
* between plan and apply refuses the apply only when it is one chant's
|
|
100
|
+
* apply changes. Server bookkeeping never reaches it (`apply-diff.ts`).
|
|
101
|
+
*/
|
|
102
|
+
function dryRunDigest(ctx, member, input, documents, dryRun) {
|
|
103
|
+
return computePlanDigest("kubectl-apply-plan", {
|
|
104
|
+
env: ctx.env,
|
|
105
|
+
member,
|
|
106
|
+
context: input.context ?? null,
|
|
107
|
+
delete: input.delete ?? "never",
|
|
108
|
+
fieldManager: dryRun.fieldManager,
|
|
109
|
+
documents,
|
|
110
|
+
objects: dryRun.objects.map((o) => ({
|
|
111
|
+
address: addressOf(o.ref),
|
|
112
|
+
type: typeOf(o.ref),
|
|
113
|
+
action: o.action,
|
|
114
|
+
changes: o.changes.map((c) => ("after" in c ? { path: c.path, after: c.after } : { path: c.path, removed: true })),
|
|
115
|
+
...(o.refused !== undefined ? { refused: o.refused } : {}),
|
|
116
|
+
})),
|
|
117
|
+
deletes: dryRun.deletes.map((r) => `${typeOf(r)}/${r.namespace ?? ""}/${r.name}`).sort(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** The value at a dot path, or `undefined`. */
|
|
121
|
+
function fieldAt(object, path) {
|
|
122
|
+
return path.split(".").reduce((acc, key) => (acc !== null && typeof acc === "object" ? acc[key] : undefined), object);
|
|
123
|
+
}
|
|
124
|
+
/** The declared ref of each output's object, keyed by output name. Throws for an object the manifest does not declare. */
|
|
125
|
+
function outputRefs(member, input, documents) {
|
|
126
|
+
const declared = new Map();
|
|
127
|
+
for (const doc of documents) {
|
|
128
|
+
const metadata = (doc.metadata ?? {});
|
|
129
|
+
const ref = {
|
|
130
|
+
apiVersion: String(doc.apiVersion ?? ""),
|
|
131
|
+
kind: String(doc.kind ?? ""),
|
|
132
|
+
name: String(metadata.name ?? ""),
|
|
133
|
+
...(metadata.namespace !== undefined ? { namespace: String(metadata.namespace) } : {}),
|
|
134
|
+
};
|
|
135
|
+
declared.set(addressOf(ref), ref);
|
|
136
|
+
}
|
|
137
|
+
const out = new Map();
|
|
138
|
+
for (const [name, spec] of Object.entries(input.outputs ?? {})) {
|
|
139
|
+
const ref = declared.get(spec.object);
|
|
140
|
+
if (!ref) {
|
|
141
|
+
throw new Error(`kubectl-apply "${member}": output "${name}" names ${spec.object}, which ${input.manifest} does not declare (it declares ${[...declared.keys()].join(", ") || "nothing"})`);
|
|
142
|
+
}
|
|
143
|
+
out.set(name, ref);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Each output's field as `objects` hold it, one object per distinct ref; an output with no value is left out. */
|
|
148
|
+
async function readOutputs(read, refs, input, where) {
|
|
149
|
+
const distinct = [...new Map([...refs.values()].map((r) => [addressOf(r), r])).values()];
|
|
150
|
+
const live = await read({ refs: distinct, ...where });
|
|
151
|
+
const byAddress = new Map(distinct.map((r, i) => [addressOf(r), live[i]]));
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [name, ref] of refs) {
|
|
154
|
+
const value = fieldAt(byAddress.get(addressOf(ref)), input.outputs[name].field);
|
|
155
|
+
if (value !== undefined)
|
|
156
|
+
out[name] = value;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/** Factory with an injectable applier, so tests assert the delegation without
|
|
161
|
+
* a cluster — the same seam `createGenerateSbomCapability` uses. The default
|
|
162
|
+
* applier is resolved by dynamic import on first run, keeping the API-client
|
|
163
|
+
* chain off the build path (#1074). */
|
|
164
|
+
export function createKubectlApplyCapability(apply, read, dryRun, readLive) {
|
|
165
|
+
const activities = () => import("../op/activities/kubectl.js");
|
|
166
|
+
const target = (ctx, input) => ({
|
|
167
|
+
environment: ctx.env,
|
|
168
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
169
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
170
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
171
|
+
});
|
|
172
|
+
/** Dry-run `documents` and digest the result. */
|
|
173
|
+
const dryRunOf = async (ctx, input, member, documents) => {
|
|
174
|
+
const planner = dryRun ?? (await activities()).planManifest;
|
|
175
|
+
const result = await planner({ manifest: input.manifest, documents, ...target(ctx, input) });
|
|
176
|
+
return { result, planDigest: dryRunDigest(ctx, member, input, documents, result) };
|
|
177
|
+
};
|
|
178
|
+
const liveReader = async () => readLive ?? (await activities()).readLiveObjects;
|
|
179
|
+
const where = (ctx, input) => ({
|
|
180
|
+
environment: ctx.env,
|
|
181
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
182
|
+
});
|
|
183
|
+
/** The outputs the step names, read from the cluster now. */
|
|
184
|
+
const liveOutputs = async (ctx, input, documents) => readOutputs(await liveReader(), outputRefs(input.stack ?? ctx.component, input, documents), input, where(ctx, input));
|
|
185
|
+
/**
|
|
186
|
+
* What the plan knows of each output: a field the manifest sets, and any
|
|
187
|
+
* field of an object the dry run leaves unchanged, read from the cluster.
|
|
188
|
+
*/
|
|
189
|
+
const plannedOutputs = async (ctx, input, member, documents, dry) => {
|
|
190
|
+
const refs = outputRefs(member, input, documents);
|
|
191
|
+
const declared = new Map(documents.map((d) => [addressOf({ apiVersion: "", kind: String(d.kind ?? ""), name: String(d.metadata?.name ?? ""), ...(d.metadata?.namespace !== undefined ? { namespace: String(d.metadata.namespace) } : {}) }), d]));
|
|
192
|
+
const unchanged = new Set(dry.objects.filter((o) => o.action === "no-op").map((o) => addressOf(o.ref)));
|
|
193
|
+
const fromLive = new Map([...refs].filter(([, ref]) => unchanged.has(addressOf(ref))));
|
|
194
|
+
const live = fromLive.size > 0 ? await readOutputs(await liveReader(), fromLive, input, where(ctx, input)) : {};
|
|
195
|
+
const known = {};
|
|
196
|
+
const unknown = [];
|
|
197
|
+
for (const [name, ref] of refs) {
|
|
198
|
+
const set = fieldAt(declared.get(addressOf(ref)), input.outputs[name].field);
|
|
199
|
+
if (set !== undefined)
|
|
200
|
+
known[name] = set;
|
|
201
|
+
else if (name in live)
|
|
202
|
+
known[name] = live[name];
|
|
203
|
+
else
|
|
204
|
+
unknown.push(name);
|
|
205
|
+
}
|
|
206
|
+
return { known, unknown: unknown.sort() };
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
kind: "kubectl-apply",
|
|
210
|
+
// A server-side apply has no native undo (the previous object state is not
|
|
211
|
+
// kept by the API server), so COMP003 requires the component to acknowledge
|
|
212
|
+
// the compensation gap — the same posture as s3-sync/run-migration.
|
|
213
|
+
rollbackPolicy: "needs-opt-out",
|
|
214
|
+
/**
|
|
215
|
+
* Dry-run the manifest against the cluster (#3998): the documents `run`
|
|
216
|
+
* would apply, release attributes set, server-side applied with
|
|
217
|
+
* `dryRun=All` as the same field manager, each compared with its live
|
|
218
|
+
* object. The member is the step's `stack`, or the component.
|
|
219
|
+
*/
|
|
220
|
+
async plan(ctx, input) {
|
|
221
|
+
const member = input.stack ?? ctx.component;
|
|
222
|
+
const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
223
|
+
const suffix = releaseAttributesSuffix(ctx.release);
|
|
224
|
+
if (suffix !== "")
|
|
225
|
+
annotateRelease(documents, suffix);
|
|
226
|
+
const { result, planDigest } = await dryRunOf(ctx, input, member, documents);
|
|
227
|
+
const artifact = { kind: "kubectl-apply-plan", planDigest, documents };
|
|
228
|
+
const outputs = input.outputs ? await plannedOutputs(ctx, input, member, documents, result) : undefined;
|
|
229
|
+
return { member, planDigest, artifact, changeSet: changeSetPartOf(member, planDigest, result), ...(outputs ? { outputs } : {}) };
|
|
230
|
+
},
|
|
231
|
+
/**
|
|
232
|
+
* The fields the step names as outputs, as the cluster holds them now
|
|
233
|
+
* (#3999). A step that names none has none; an object not made yet
|
|
234
|
+
* leaves its outputs out.
|
|
235
|
+
*/
|
|
236
|
+
async outputs(ctx, input) {
|
|
237
|
+
if (!input.outputs || Object.keys(input.outputs).length === 0)
|
|
238
|
+
return {};
|
|
239
|
+
const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
240
|
+
return liveOutputs(ctx, input, documents);
|
|
241
|
+
},
|
|
242
|
+
async run(ctx, input) {
|
|
243
|
+
const member = input.stack ?? ctx.component;
|
|
244
|
+
const approved = ctx.plans?.[member];
|
|
245
|
+
if (isPlanArtifact(approved)) {
|
|
246
|
+
// Apply the documents the approver saw, and only while a fresh dry
|
|
247
|
+
// run of them still says what the approved one said.
|
|
248
|
+
const { planDigest } = await dryRunOf(ctx, input, member, approved.documents);
|
|
249
|
+
if (planDigest !== approved.planDigest)
|
|
250
|
+
throw new KubectlApplyPlanMovedError(member, approved.planDigest, planDigest);
|
|
251
|
+
const applier = apply ?? (await activities()).applyManifest;
|
|
252
|
+
const outcome = await applier({ manifest: input.manifest, documents: approved.documents, ...target(ctx, input) });
|
|
253
|
+
return input.outputs ? { ...outcome, outputs: await liveOutputs(ctx, input, approved.documents) } : outcome;
|
|
254
|
+
}
|
|
255
|
+
const loaded = apply && read ? undefined : await activities();
|
|
256
|
+
const applier = apply ?? loaded.applyManifest;
|
|
257
|
+
// A release run (#3061, ws-081) sets the release attributes on the pod template of each
|
|
258
|
+
// workload the build stamped, in what it applies; the committed manifest is not touched.
|
|
259
|
+
const suffix = releaseAttributesSuffix(ctx.release);
|
|
260
|
+
let documents;
|
|
261
|
+
if (suffix !== "") {
|
|
262
|
+
documents = (read ?? loaded.readManifestDocuments)(input.manifest);
|
|
263
|
+
if (annotateRelease(documents, suffix) === 0)
|
|
264
|
+
documents = undefined;
|
|
265
|
+
}
|
|
266
|
+
const outcome = await applier({ manifest: input.manifest, ...(documents ? { documents } : {}), ...target(ctx, input) });
|
|
267
|
+
if (!input.outputs)
|
|
268
|
+
return outcome;
|
|
269
|
+
const declared = documents ?? (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
270
|
+
return { ...outcome, outputs: await liveOutputs(ctx, input, declared) };
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
export const kubectlApplyCapability = createKubectlApplyCapability();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defaultKustomizeRunner, renderCommand, renderKustomizeDocuments, } from "../kustomize/render.js";
|
|
2
|
+
export { renderCommand };
|
|
3
|
+
/** Factory with injectable renderer + applier, so tests assert the render
|
|
4
|
+
* command and the exact applier delegation without kustomize or a cluster. */
|
|
5
|
+
export function createKustomizeApplyCapability(run = defaultKustomizeRunner, apply) {
|
|
6
|
+
return {
|
|
7
|
+
kind: "kustomize-apply",
|
|
8
|
+
// Server-side apply keeps no previous object state, and kustomize adds no
|
|
9
|
+
// undo of its own — same posture as kubectl-apply.
|
|
10
|
+
rollbackPolicy: "needs-opt-out",
|
|
11
|
+
async run(ctx, input) {
|
|
12
|
+
const documents = await renderKustomizeDocuments(input.dir, run);
|
|
13
|
+
const applier = apply ?? (await import("../op/activities/kubectl.js")).applyManifest;
|
|
14
|
+
return applier({
|
|
15
|
+
manifest: `kustomize:${input.dir}`,
|
|
16
|
+
documents,
|
|
17
|
+
environment: ctx.env,
|
|
18
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
19
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
20
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const kustomizeApplyCapability = createKustomizeApplyCapability();
|