@intentius/chant-lexicon-k8s 0.124.31 → 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
|
@@ -14,10 +14,25 @@
|
|
|
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
|
-
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
30
|
+
import type { Capability, CapabilityPlan, DeployContext, PlannedOutputs } from "@intentius/chant/components/capability";
|
|
31
|
+
import type { ChangeSetAttribute, ChangeSetEntry, ChangeSetHole, ChangeSetPart } from "@intentius/chant/change-set";
|
|
32
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
19
33
|
import { releaseAttributesSuffix } from "@intentius/chant/telemetry-attribution";
|
|
20
34
|
import { annotateRelease } from "../telemetry-release";
|
|
35
|
+
import { diffFields, normalizeServerObject } from "../op/activities/apply-diff";
|
|
21
36
|
|
|
22
37
|
/**
|
|
23
38
|
* Structural mirrors of the activity module's argument/result shapes.
|
|
@@ -49,6 +64,8 @@ export interface KubectlApplyOutcome {
|
|
|
49
64
|
* enter the prunable set. Present whenever the prune ran.
|
|
50
65
|
*/
|
|
51
66
|
retained?: AppliedRef[];
|
|
67
|
+
/** The fields the step names as outputs (#3999), read once the apply landed. Absent when it names none. */
|
|
68
|
+
outputs?: Record<string, unknown>;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
interface ApplierArgs {
|
|
@@ -64,6 +81,184 @@ interface ApplierArgs {
|
|
|
64
81
|
type Applier = (args: ApplierArgs) => Promise<KubectlApplyOutcome>;
|
|
65
82
|
type ManifestReader = (path: string) => Record<string, unknown>[];
|
|
66
83
|
|
|
84
|
+
/** One field change, the mirror of `../op/activities/apply-diff.ts`'s `FieldChange`. */
|
|
85
|
+
interface FieldChangeShape {
|
|
86
|
+
path: string;
|
|
87
|
+
before?: unknown;
|
|
88
|
+
after?: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The mirror of the activity's `PlannedObject`. */
|
|
92
|
+
interface PlannedObjectShape {
|
|
93
|
+
ref: AppliedRef;
|
|
94
|
+
action: "create" | "update" | "no-op";
|
|
95
|
+
object?: Record<string, unknown>;
|
|
96
|
+
changes: FieldChangeShape[];
|
|
97
|
+
refused?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** What a dry run found: the mirror of the activity's `ManifestPlanResult` (#3998). */
|
|
101
|
+
export interface KubectlApplyDryRun {
|
|
102
|
+
fieldManager: string;
|
|
103
|
+
stack?: string;
|
|
104
|
+
objects: PlannedObjectShape[];
|
|
105
|
+
deletes: AppliedRef[];
|
|
106
|
+
retained: AppliedRef[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type Planner = (args: ApplierArgs) => Promise<KubectlApplyDryRun>;
|
|
110
|
+
|
|
111
|
+
/** The mirror of the activity's `ReadLiveObjectsArgs`. */
|
|
112
|
+
interface LiveReaderArgs {
|
|
113
|
+
refs: AppliedRef[];
|
|
114
|
+
environment?: string;
|
|
115
|
+
context?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Reads objects as the cluster holds them, `undefined` for one that does not exist (#3999). */
|
|
119
|
+
type LiveReader = (args: LiveReaderArgs) => Promise<Array<Record<string, unknown> | undefined>>;
|
|
120
|
+
|
|
121
|
+
/** What `plan` hands `run` through `DeployContext.plans` (#3998). */
|
|
122
|
+
export interface KubectlApplyPlanArtifact {
|
|
123
|
+
kind: "kubectl-apply-plan";
|
|
124
|
+
planDigest: string;
|
|
125
|
+
/** The documents the plan dry-ran, release attributes set. `run` applies these. */
|
|
126
|
+
documents: Record<string, unknown>[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isPlanArtifact(value: unknown): value is KubectlApplyPlanArtifact {
|
|
130
|
+
const v = value as Partial<KubectlApplyPlanArtifact> | undefined;
|
|
131
|
+
return !!v && v.kind === "kubectl-apply-plan" && typeof v.planDigest === "string" && Array.isArray(v.documents);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `run` was handed an approved plan and a fresh dry run of the same
|
|
136
|
+
* documents gives a different digest: the cluster moved since the plan, or
|
|
137
|
+
* the plan was made against another one. Nothing is applied. `planMoved` is
|
|
138
|
+
* core's mark for a moved plan (`isPlanMovedError`), so the command that
|
|
139
|
+
* applied it exits 3. `exitCode` 3 says the same to a core that reads it.
|
|
140
|
+
*/
|
|
141
|
+
export class KubectlApplyPlanMovedError extends Error {
|
|
142
|
+
readonly planMoved = true;
|
|
143
|
+
readonly exitCode = 3;
|
|
144
|
+
constructor(
|
|
145
|
+
public readonly member: string,
|
|
146
|
+
public readonly approved: string,
|
|
147
|
+
public readonly fresh: string,
|
|
148
|
+
) {
|
|
149
|
+
super(
|
|
150
|
+
`kubectl-apply "${member}": the plan moved since it was approved (approved ${approved}, now ${fresh}); ` +
|
|
151
|
+
`nothing was applied. Plan and approve again.`,
|
|
152
|
+
);
|
|
153
|
+
this.name = "KubectlApplyPlanMovedError";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Kinds whose pod template change rolls their pods. */
|
|
158
|
+
const ROLLING_KINDS = new Set(["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"]);
|
|
159
|
+
|
|
160
|
+
/** `Deployment/prod/web`, or `ClusterRole/admin` for a cluster-scoped object. */
|
|
161
|
+
function addressOf(ref: AppliedRef): string {
|
|
162
|
+
return ref.namespace ? `${ref.kind}/${ref.namespace}/${ref.name}` : `${ref.kind}/${ref.name}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const typeOf = (ref: AppliedRef): string => `${ref.apiVersion}/${ref.kind}`;
|
|
166
|
+
|
|
167
|
+
/** A Secret's `data` and `stringData` values are never carried into the change set. */
|
|
168
|
+
function isSensitive(ref: AppliedRef, path: string): boolean {
|
|
169
|
+
return ref.kind === "Secret" && /^(data|stringData)\b/.test(path);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function attribute(ref: AppliedRef, change: FieldChangeShape): ChangeSetAttribute {
|
|
173
|
+
if (isSensitive(ref, change.path)) return { path: change.path, sensitive: true };
|
|
174
|
+
return {
|
|
175
|
+
path: change.path,
|
|
176
|
+
...("before" in change ? { before: change.before } : {}),
|
|
177
|
+
...("after" in change ? { after: change.after } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The member's change-set part: one entry per declared object (`no-op`
|
|
183
|
+
* included, as the terraform adapter reports them) and one `delete` per
|
|
184
|
+
* object the prune would remove. A change the API server refuses as
|
|
185
|
+
* immutable is an `update` with disruption `unknown`, plus a hole that says
|
|
186
|
+
* the apply would fail.
|
|
187
|
+
*/
|
|
188
|
+
function changeSetPartOf(member: string, planDigest: string, dryRun: KubectlApplyDryRun): ChangeSetPart {
|
|
189
|
+
const base = { member, lexicon: "k8s", planner: "chant" as const };
|
|
190
|
+
const entries: ChangeSetEntry[] = [];
|
|
191
|
+
const holes: ChangeSetHole[] = [];
|
|
192
|
+
for (const o of dryRun.objects) {
|
|
193
|
+
const head = { ...base, address: addressOf(o.ref), type: typeOf(o.ref), name: o.ref.name };
|
|
194
|
+
if (o.refused !== undefined) {
|
|
195
|
+
entries.push({ ...head, action: "update", disruption: "unknown", attributes: [] });
|
|
196
|
+
holes.push({
|
|
197
|
+
address: head.address,
|
|
198
|
+
type: head.type,
|
|
199
|
+
reason:
|
|
200
|
+
`the API server refuses the change: ${o.refused}. The apply does not delete and recreate ` +
|
|
201
|
+
`the object, so it fails the same way until the object is deleted`,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (o.action === "create") {
|
|
206
|
+
const declared = normalizeServerObject(o.object ?? {});
|
|
207
|
+
delete declared.apiVersion;
|
|
208
|
+
delete declared.kind;
|
|
209
|
+
entries.push({ ...head, action: "create", attributes: diffFields({}, declared).map((c) => attribute(o.ref, c)) });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (o.action === "no-op") {
|
|
213
|
+
entries.push({ ...head, action: "no-op", attributes: [] });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const rolling = ROLLING_KINDS.has(o.ref.kind) && o.changes.some((c) => c.path.startsWith("spec.template"));
|
|
217
|
+
entries.push({
|
|
218
|
+
...head,
|
|
219
|
+
action: "update",
|
|
220
|
+
disruption: rolling ? "rolling" : "in-place",
|
|
221
|
+
attributes: o.changes.map((c) => attribute(o.ref, c)),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
for (const ref of dryRun.deletes) {
|
|
225
|
+
entries.push({ ...base, address: addressOf(ref), type: typeOf(ref), name: ref.name, action: "delete", disruption: "destroy", attributes: [] });
|
|
226
|
+
}
|
|
227
|
+
return { member: { ...base, status: "planned", planDigest, holes }, entries };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* What a gate on this member binds: where it applies, as whom, the documents
|
|
232
|
+
* sent, and what the dry run says each does. For an object it changes, the
|
|
233
|
+
* changed paths and their new values, so a field another manager moves
|
|
234
|
+
* between plan and apply refuses the apply only when it is one chant's
|
|
235
|
+
* apply changes. Server bookkeeping never reaches it (`apply-diff.ts`).
|
|
236
|
+
*/
|
|
237
|
+
function dryRunDigest(
|
|
238
|
+
ctx: DeployContext,
|
|
239
|
+
member: string,
|
|
240
|
+
input: KubectlApplyInput,
|
|
241
|
+
documents: Record<string, unknown>[],
|
|
242
|
+
dryRun: KubectlApplyDryRun,
|
|
243
|
+
): string {
|
|
244
|
+
return computePlanDigest("kubectl-apply-plan", {
|
|
245
|
+
env: ctx.env,
|
|
246
|
+
member,
|
|
247
|
+
context: input.context ?? null,
|
|
248
|
+
delete: input.delete ?? "never",
|
|
249
|
+
fieldManager: dryRun.fieldManager,
|
|
250
|
+
documents,
|
|
251
|
+
objects: dryRun.objects.map((o) => ({
|
|
252
|
+
address: addressOf(o.ref),
|
|
253
|
+
type: typeOf(o.ref),
|
|
254
|
+
action: o.action,
|
|
255
|
+
changes: o.changes.map((c) => ("after" in c ? { path: c.path, after: c.after } : { path: c.path, removed: true })),
|
|
256
|
+
...(o.refused !== undefined ? { refused: o.refused } : {}),
|
|
257
|
+
})),
|
|
258
|
+
deletes: dryRun.deletes.map((r) => `${typeOf(r)}/${r.namespace ?? ""}/${r.name}`).sort(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
67
262
|
export interface KubectlApplyInput {
|
|
68
263
|
/** Path to a manifest file, or a directory of them. */
|
|
69
264
|
manifest: string;
|
|
@@ -83,6 +278,64 @@ export interface KubectlApplyInput {
|
|
|
83
278
|
* candidate.
|
|
84
279
|
*/
|
|
85
280
|
delete?: "never" | "owned-only";
|
|
281
|
+
/**
|
|
282
|
+
* The outputs the step exposes (#3999), by name: each a field of an object
|
|
283
|
+
* the manifest declares. A dependent reads them with `stackOutput()`.
|
|
284
|
+
*/
|
|
285
|
+
outputs?: Record<string, KubectlApplyOutputField>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** One output of a `kubectl-apply` step (#3999). */
|
|
289
|
+
export interface KubectlApplyOutputField {
|
|
290
|
+
/**
|
|
291
|
+
* The object, named as the manifest declares it: `Kind/namespace/name`, or
|
|
292
|
+
* `Kind/name` for a document with no namespace.
|
|
293
|
+
*/
|
|
294
|
+
object: string;
|
|
295
|
+
/** The field's dot path in the object; a list index is a number, `status.loadBalancer.ingress.0.hostname`. */
|
|
296
|
+
field: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The value at a dot path, or `undefined`. */
|
|
300
|
+
function fieldAt(object: unknown, path: string): unknown {
|
|
301
|
+
return path.split(".").reduce<unknown>((acc, key) => (acc !== null && typeof acc === "object" ? (acc as Record<string, unknown>)[key] : undefined), object);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The declared ref of each output's object, keyed by output name. Throws for an object the manifest does not declare. */
|
|
305
|
+
function outputRefs(member: string, input: KubectlApplyInput, documents: Record<string, unknown>[]): Map<string, AppliedRef> {
|
|
306
|
+
const declared = new Map<string, AppliedRef>();
|
|
307
|
+
for (const doc of documents) {
|
|
308
|
+
const metadata = (doc.metadata ?? {}) as { name?: unknown; namespace?: unknown };
|
|
309
|
+
const ref: AppliedRef = {
|
|
310
|
+
apiVersion: String(doc.apiVersion ?? ""),
|
|
311
|
+
kind: String(doc.kind ?? ""),
|
|
312
|
+
name: String(metadata.name ?? ""),
|
|
313
|
+
...(metadata.namespace !== undefined ? { namespace: String(metadata.namespace) } : {}),
|
|
314
|
+
};
|
|
315
|
+
declared.set(addressOf(ref), ref);
|
|
316
|
+
}
|
|
317
|
+
const out = new Map<string, AppliedRef>();
|
|
318
|
+
for (const [name, spec] of Object.entries(input.outputs ?? {})) {
|
|
319
|
+
const ref = declared.get(spec.object);
|
|
320
|
+
if (!ref) {
|
|
321
|
+
throw new Error(`kubectl-apply "${member}": output "${name}" names ${spec.object}, which ${input.manifest} does not declare (it declares ${[...declared.keys()].join(", ") || "nothing"})`);
|
|
322
|
+
}
|
|
323
|
+
out.set(name, ref);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Each output's field as `objects` hold it, one object per distinct ref; an output with no value is left out. */
|
|
329
|
+
async function readOutputs(read: LiveReader, refs: Map<string, AppliedRef>, input: KubectlApplyInput, where: Omit<LiveReaderArgs, "refs">): Promise<Record<string, unknown>> {
|
|
330
|
+
const distinct = [...new Map([...refs.values()].map((r) => [addressOf(r), r])).values()];
|
|
331
|
+
const live = await read({ refs: distinct, ...where });
|
|
332
|
+
const byAddress = new Map(distinct.map((r, i) => [addressOf(r), live[i]]));
|
|
333
|
+
const out: Record<string, unknown> = {};
|
|
334
|
+
for (const [name, ref] of refs) {
|
|
335
|
+
const value = fieldAt(byAddress.get(addressOf(ref)), input.outputs![name]!.field);
|
|
336
|
+
if (value !== undefined) out[name] = value;
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
86
339
|
}
|
|
87
340
|
|
|
88
341
|
/** Factory with an injectable applier, so tests assert the delegation without
|
|
@@ -92,32 +345,108 @@ export interface KubectlApplyInput {
|
|
|
92
345
|
export function createKubectlApplyCapability(
|
|
93
346
|
apply?: Applier,
|
|
94
347
|
read?: ManifestReader,
|
|
348
|
+
dryRun?: Planner,
|
|
349
|
+
readLive?: LiveReader,
|
|
95
350
|
): Capability<KubectlApplyInput, KubectlApplyOutcome> {
|
|
351
|
+
const activities = () => import("../op/activities/kubectl");
|
|
352
|
+
const target = (ctx: DeployContext, input: KubectlApplyInput) => ({
|
|
353
|
+
environment: ctx.env,
|
|
354
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
355
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
356
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
357
|
+
});
|
|
358
|
+
/** Dry-run `documents` and digest the result. */
|
|
359
|
+
const dryRunOf = async (ctx: DeployContext, input: KubectlApplyInput, member: string, documents: Record<string, unknown>[]) => {
|
|
360
|
+
const planner: Planner = dryRun ?? (await activities()).planManifest;
|
|
361
|
+
const result = await planner({ manifest: input.manifest, documents, ...target(ctx, input) });
|
|
362
|
+
return { result, planDigest: dryRunDigest(ctx, member, input, documents, result) };
|
|
363
|
+
};
|
|
364
|
+
const liveReader = async (): Promise<LiveReader> => readLive ?? (await activities()).readLiveObjects;
|
|
365
|
+
const where = (ctx: DeployContext, input: KubectlApplyInput) => ({
|
|
366
|
+
environment: ctx.env,
|
|
367
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
368
|
+
});
|
|
369
|
+
/** The outputs the step names, read from the cluster now. */
|
|
370
|
+
const liveOutputs = async (ctx: DeployContext, input: KubectlApplyInput, documents: Record<string, unknown>[]) =>
|
|
371
|
+
readOutputs(await liveReader(), outputRefs(input.stack ?? ctx.component, input, documents), input, where(ctx, input));
|
|
372
|
+
/**
|
|
373
|
+
* What the plan knows of each output: a field the manifest sets, and any
|
|
374
|
+
* field of an object the dry run leaves unchanged, read from the cluster.
|
|
375
|
+
*/
|
|
376
|
+
const plannedOutputs = async (ctx: DeployContext, input: KubectlApplyInput, member: string, documents: Record<string, unknown>[], dry: KubectlApplyDryRun): Promise<PlannedOutputs> => {
|
|
377
|
+
const refs = outputRefs(member, input, documents);
|
|
378
|
+
const declared = new Map(documents.map((d) => [addressOf({ apiVersion: "", kind: String(d.kind ?? ""), name: String((d.metadata as { name?: unknown } | undefined)?.name ?? ""), ...((d.metadata as { namespace?: unknown } | undefined)?.namespace !== undefined ? { namespace: String((d.metadata as { namespace: unknown }).namespace) } : {}) }), d]));
|
|
379
|
+
const unchanged = new Set(dry.objects.filter((o) => o.action === "no-op").map((o) => addressOf(o.ref)));
|
|
380
|
+
const fromLive = new Map([...refs].filter(([, ref]) => unchanged.has(addressOf(ref))));
|
|
381
|
+
const live = fromLive.size > 0 ? await readOutputs(await liveReader(), fromLive, input, where(ctx, input)) : {};
|
|
382
|
+
const known: Record<string, unknown> = {};
|
|
383
|
+
const unknown: string[] = [];
|
|
384
|
+
for (const [name, ref] of refs) {
|
|
385
|
+
const set = fieldAt(declared.get(addressOf(ref)), input.outputs![name]!.field);
|
|
386
|
+
if (set !== undefined) known[name] = set;
|
|
387
|
+
else if (name in live) known[name] = live[name];
|
|
388
|
+
else unknown.push(name);
|
|
389
|
+
}
|
|
390
|
+
return { known, unknown: unknown.sort() };
|
|
391
|
+
};
|
|
96
392
|
return {
|
|
97
393
|
kind: "kubectl-apply",
|
|
98
394
|
// A server-side apply has no native undo (the previous object state is not
|
|
99
395
|
// kept by the API server), so COMP003 requires the component to acknowledge
|
|
100
396
|
// the compensation gap — the same posture as s3-sync/run-migration.
|
|
101
397
|
rollbackPolicy: "needs-opt-out",
|
|
398
|
+
/**
|
|
399
|
+
* Dry-run the manifest against the cluster (#3998): the documents `run`
|
|
400
|
+
* would apply, release attributes set, server-side applied with
|
|
401
|
+
* `dryRun=All` as the same field manager, each compared with its live
|
|
402
|
+
* object. The member is the step's `stack`, or the component.
|
|
403
|
+
*/
|
|
404
|
+
async plan(ctx: DeployContext, input: KubectlApplyInput): Promise<CapabilityPlan> {
|
|
405
|
+
const member = input.stack ?? ctx.component;
|
|
406
|
+
const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
407
|
+
const suffix = releaseAttributesSuffix(ctx.release);
|
|
408
|
+
if (suffix !== "") annotateRelease(documents, suffix);
|
|
409
|
+
const { result, planDigest } = await dryRunOf(ctx, input, member, documents);
|
|
410
|
+
const artifact: KubectlApplyPlanArtifact = { kind: "kubectl-apply-plan", planDigest, documents };
|
|
411
|
+
const outputs = input.outputs ? await plannedOutputs(ctx, input, member, documents, result) : undefined;
|
|
412
|
+
return { member, planDigest, artifact, changeSet: changeSetPartOf(member, planDigest, result), ...(outputs ? { outputs } : {}) };
|
|
413
|
+
},
|
|
414
|
+
/**
|
|
415
|
+
* The fields the step names as outputs, as the cluster holds them now
|
|
416
|
+
* (#3999). A step that names none has none; an object not made yet
|
|
417
|
+
* leaves its outputs out.
|
|
418
|
+
*/
|
|
419
|
+
async outputs(ctx: DeployContext, input: KubectlApplyInput): Promise<Record<string, unknown>> {
|
|
420
|
+
if (!input.outputs || Object.keys(input.outputs).length === 0) return {};
|
|
421
|
+
const documents = (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
422
|
+
return liveOutputs(ctx, input, documents);
|
|
423
|
+
},
|
|
102
424
|
async run(ctx: DeployContext, input: KubectlApplyInput): Promise<KubectlApplyOutcome> {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
425
|
+
const member = input.stack ?? ctx.component;
|
|
426
|
+
const approved = ctx.plans?.[member];
|
|
427
|
+
if (isPlanArtifact(approved)) {
|
|
428
|
+
// Apply the documents the approver saw, and only while a fresh dry
|
|
429
|
+
// run of them still says what the approved one said.
|
|
430
|
+
const { planDigest } = await dryRunOf(ctx, input, member, approved.documents);
|
|
431
|
+
if (planDigest !== approved.planDigest) throw new KubectlApplyPlanMovedError(member, approved.planDigest, planDigest);
|
|
432
|
+
const applier: Applier = apply ?? (await activities()).applyManifest;
|
|
433
|
+
const outcome = await applier({ manifest: input.manifest, documents: approved.documents, ...target(ctx, input) });
|
|
434
|
+
return input.outputs ? { ...outcome, outputs: await liveOutputs(ctx, input, approved.documents) } : outcome;
|
|
435
|
+
}
|
|
436
|
+
const loaded = apply && read ? undefined : await activities();
|
|
437
|
+
const applier: Applier = apply ?? loaded!.applyManifest;
|
|
105
438
|
// A release run (#3061, ws-081) sets the release attributes on the pod template of each
|
|
106
439
|
// workload the build stamped, in what it applies; the committed manifest is not touched.
|
|
107
440
|
const suffix = releaseAttributesSuffix(ctx.release);
|
|
108
441
|
let documents: Record<string, unknown>[] | undefined;
|
|
109
442
|
if (suffix !== "") {
|
|
110
|
-
documents = (read ??
|
|
443
|
+
documents = (read ?? loaded!.readManifestDocuments)(input.manifest);
|
|
111
444
|
if (annotateRelease(documents, suffix) === 0) documents = undefined;
|
|
112
445
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
118
|
-
...(input.context !== undefined ? { context: input.context } : {}),
|
|
119
|
-
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
120
|
-
});
|
|
446
|
+
const outcome = await applier({ manifest: input.manifest, ...(documents ? { documents } : {}), ...target(ctx, input) });
|
|
447
|
+
if (!input.outputs) return outcome;
|
|
448
|
+
const declared = documents ?? (read ?? (await activities()).readManifestDocuments)(input.manifest);
|
|
449
|
+
return { ...outcome, outputs: await liveOutputs(ctx, input, declared) };
|
|
121
450
|
},
|
|
122
451
|
};
|
|
123
452
|
}
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
import { existsSync, readFileSync } from "fs";
|
|
49
49
|
import { dirname, join } from "path";
|
|
50
|
-
import {
|
|
50
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
51
51
|
import type { DeepArrayElement, DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
52
52
|
import {
|
|
53
53
|
K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS,
|
|
@@ -202,7 +202,7 @@ let cachedListMapKeys: ListMapKeyTable | undefined;
|
|
|
202
202
|
* class of bug in #1581. A plain fs read has no module-system dependency.
|
|
203
203
|
*/
|
|
204
204
|
export function listMapKeyTablePath(): string {
|
|
205
|
-
return join(
|
|
205
|
+
return join(moduleSourceDir(import.meta.url), "generated", "list-map-keys.json");
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
@@ -84,4 +84,50 @@ describe("k8s import round trip", () => {
|
|
|
84
84
|
}
|
|
85
85
|
expect(await roundTrip(yaml)).toEqual(loadAll(yaml));
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
test("import names an error-severity check the generated source will fail at build (#3605)", async () => {
|
|
89
|
+
const yaml = [
|
|
90
|
+
"apiVersion: apps/v1",
|
|
91
|
+
"kind: StatefulSet",
|
|
92
|
+
"metadata:",
|
|
93
|
+
" name: mysql",
|
|
94
|
+
"spec:",
|
|
95
|
+
" selector:",
|
|
96
|
+
" matchLabels:",
|
|
97
|
+
" app: mysql",
|
|
98
|
+
" serviceName: mysql",
|
|
99
|
+
" template:",
|
|
100
|
+
" metadata:",
|
|
101
|
+
" labels:",
|
|
102
|
+
" app: mysql",
|
|
103
|
+
" spec:",
|
|
104
|
+
" containers:",
|
|
105
|
+
" - name: mysql",
|
|
106
|
+
" image: mysql:5.7",
|
|
107
|
+
" env:",
|
|
108
|
+
" - name: MYSQL_ALLOW_EMPTY_PASSWORD",
|
|
109
|
+
' value: "1"',
|
|
110
|
+
].join("\n");
|
|
111
|
+
const imported = await importManifest(yaml);
|
|
112
|
+
try {
|
|
113
|
+
expect(imported.result.success).toBe(true);
|
|
114
|
+
const named = imported.result.warnings.filter((w) => w.startsWith("WK8005:"));
|
|
115
|
+
expect(named).toHaveLength(1);
|
|
116
|
+
expect(named[0]).toContain('"MYSQL_ALLOW_EMPTY_PASSWORD"');
|
|
117
|
+
expect(named[0]).toContain("chant build stops on this error");
|
|
118
|
+
} finally {
|
|
119
|
+
removeDir(imported.dir);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("import names no check for a manifest that builds cleanly", async () => {
|
|
124
|
+
const imported = await importManifest(
|
|
125
|
+
["apiVersion: v1", "kind: ConfigMap", "metadata:", " name: settings", "data:", " mode: fast"].join("\n"),
|
|
126
|
+
);
|
|
127
|
+
try {
|
|
128
|
+
expect(imported.result.warnings.filter((w) => /^[A-Z]+\d+: /.test(w))).toEqual([]);
|
|
129
|
+
} finally {
|
|
130
|
+
removeDir(imported.dir);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
87
133
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { createRequire } from "module";
|
|
15
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
15
16
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
16
17
|
import type { CrdFieldSchema } from "../../spec/parse";
|
|
17
18
|
import { docsToManifests, type K8sManifest } from "./k8s-helpers";
|
|
@@ -44,7 +45,7 @@ export function getCrdSchemaRegistry(): Map<string, CrdFieldSchema> {
|
|
|
44
45
|
// Built lazily so importing this module stays edge-safe: `createRequire`
|
|
45
46
|
// throws where import.meta.url is undefined (bundled Workers), and there
|
|
46
47
|
// the registry is simply empty.
|
|
47
|
-
const require = createRequire(import.meta.url);
|
|
48
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
48
49
|
const lexicon = require("../../generated/lexicon-k8s.json") as Record<string, LexiconEntry>;
|
|
49
50
|
for (const entry of Object.values(lexicon)) {
|
|
50
51
|
if (entry.kind === "resource" && entry.apiVersion && entry.gvkKind && entry.specSchema) {
|
package/src/lsp/completions.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|
package/src/lsp/hover.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
2
3
|
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
3
4
|
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
6
|
|
|
6
7
|
let cachedIndex: LexiconIndex | null = null;
|
|
7
8
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a server-side apply would change in one object (#3998): the live
|
|
3
|
+
* object against the result of a dry-run apply of the declared one.
|
|
4
|
+
*
|
|
5
|
+
* Both sides come from the API server, so a field the server defaults or
|
|
6
|
+
* fills in appears on both with the same value and drops out of the diff.
|
|
7
|
+
* What is left out before comparing is the bookkeeping the server writes on
|
|
8
|
+
* every request and that says nothing about the declared state: managed
|
|
9
|
+
* fields, the resource version, the uid, the creation timestamp, the
|
|
10
|
+
* generation (a dry run of a spec change bumps it) and `status`.
|
|
11
|
+
*
|
|
12
|
+
* Pure functions only, with no client import, so the capability module can
|
|
13
|
+
* share them without reaching the API-client chain (#1074).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One field whose value the apply would change. `before` is absent on an added field, `after` on a removed one. */
|
|
17
|
+
export interface FieldChange {
|
|
18
|
+
path: string;
|
|
19
|
+
before?: unknown;
|
|
20
|
+
after?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The `metadata` keys the server writes on every request. */
|
|
24
|
+
const SERVER_METADATA = ["managedFields", "resourceVersion", "uid", "creationTimestamp", "generation", "selfLink"] as const;
|
|
25
|
+
|
|
26
|
+
/** A copy of `object` without the fields the server writes on every request. */
|
|
27
|
+
export function normalizeServerObject(object: Record<string, unknown>): Record<string, unknown> {
|
|
28
|
+
const { status: _status, ...rest } = object;
|
|
29
|
+
const metadata = rest.metadata;
|
|
30
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
31
|
+
const copy: Record<string, unknown> = { ...(metadata as Record<string, unknown>) };
|
|
32
|
+
for (const key of SERVER_METADATA) delete copy[key];
|
|
33
|
+
rest.metadata = copy;
|
|
34
|
+
}
|
|
35
|
+
return rest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
39
|
+
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
40
|
+
|
|
41
|
+
/** Deep equality over JSON values. */
|
|
42
|
+
export function jsonEqual(a: unknown, b: unknown): boolean {
|
|
43
|
+
if (a === b) return true;
|
|
44
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
|
|
45
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
46
|
+
const ka = Object.keys(a);
|
|
47
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
48
|
+
return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `spec.replicas`, or `metadata.labels["app.kubernetes.io/name"]` for a key that is not a plain identifier. */
|
|
54
|
+
function joinPath(prefix: string, key: string): string {
|
|
55
|
+
if (/^[A-Za-z_$][\w$-]*$/.test(key)) return prefix ? `${prefix}.${key}` : key;
|
|
56
|
+
return `${prefix}[${JSON.stringify(key)}]`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The fields that differ between `live` and `next`, both normalized. Objects
|
|
61
|
+
* are walked to their leaves; a list is compared whole and reported at its
|
|
62
|
+
* own path, since a list's items have no stable address in general.
|
|
63
|
+
*/
|
|
64
|
+
export function diffFields(live: Record<string, unknown>, next: Record<string, unknown>, prefix = ""): FieldChange[] {
|
|
65
|
+
const changes: FieldChange[] = [];
|
|
66
|
+
const keys = [...new Set([...Object.keys(live), ...Object.keys(next)])].sort();
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
const path = joinPath(prefix, key);
|
|
69
|
+
const hasBefore = Object.prototype.hasOwnProperty.call(live, key);
|
|
70
|
+
const hasAfter = Object.prototype.hasOwnProperty.call(next, key);
|
|
71
|
+
const before = live[key];
|
|
72
|
+
const after = next[key];
|
|
73
|
+
// An object on both sides, or added or removed whole, is walked to its
|
|
74
|
+
// leaves. An empty object added or removed has none, so it stands as itself.
|
|
75
|
+
const walkable = (has: boolean, v: unknown) => !has || isPlainObject(v);
|
|
76
|
+
if ((isPlainObject(before) || isPlainObject(after)) && walkable(hasBefore, before) && walkable(hasAfter, after)) {
|
|
77
|
+
const inner = diffFields(hasBefore ? (before as Record<string, unknown>) : {}, hasAfter ? (after as Record<string, unknown>) : {}, path);
|
|
78
|
+
if (inner.length > 0 || hasBefore === hasAfter) {
|
|
79
|
+
changes.push(...inner);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (hasBefore && hasAfter && jsonEqual(before, after)) continue;
|
|
84
|
+
changes.push({ path, ...(hasBefore ? { before } : {}), ...(hasAfter ? { after } : {}) });
|
|
85
|
+
}
|
|
86
|
+
return changes;
|
|
87
|
+
}
|