@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,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s lexicon's *static* deep-observation noise rules (#1076, epic #1073).
|
|
3
|
+
*
|
|
4
|
+
* Split out from `./deep-observe.ts` for one reason: this file must be safe
|
|
5
|
+
* to import from `plugin.ts` at module load time, because
|
|
6
|
+
* `LexiconPlugin.deepNormalizationHooks` is plain data core reads to
|
|
7
|
+
* normalize the *declared* tree — the half of the contract that runs whether
|
|
8
|
+
* or not a cluster is ever touched (`lifecycle diff` without `--live`,
|
|
9
|
+
* `chant build`, tests that only exercise normalization). `./deep-observe.ts`
|
|
10
|
+
* itself imports `@intentius/chant-k8s-client` for the live read, and chant
|
|
11
|
+
* #1074 made that package's reachability from the build path a structural
|
|
12
|
+
* property (`examples/k8s-client-boundary.test.ts`) rather than a lint rule —
|
|
13
|
+
* so nothing this file exports may pull that package in, directly or
|
|
14
|
+
* transitively. It imports the core contract's own types, and one generated
|
|
15
|
+
* JSON data file (`./generated/list-map-keys.json`, chant #1441) — data, not a
|
|
16
|
+
* package, so the boundary property is unchanged.
|
|
17
|
+
*
|
|
18
|
+
* What lives here is deliberately the *entityType-keyed, resource-agnostic*
|
|
19
|
+
* half of the rules: which fields the API server always populates regardless
|
|
20
|
+
* of what a manager wrote (`status`, `metadata.uid`, …), which fields
|
|
21
|
+
* Kubernetes defaults when a manifest is silent about them, and which arrays
|
|
22
|
+
* are sets addressed by a well-known identity (containers by name, ports by
|
|
23
|
+
* containerPort+protocol). None of that needs a live object in hand.
|
|
24
|
+
*
|
|
25
|
+
* What does *not* live here is the managed-fields ownership walk — which
|
|
26
|
+
* manager owns one specific field on one specific live object. That is
|
|
27
|
+
* inherently per-object (it depends on *that* object's
|
|
28
|
+
* `metadata.managedFields`, which the declared tree never carries and which
|
|
29
|
+
* differs between two Deployments of the same type), so it cannot be
|
|
30
|
+
* expressed as a fixed rule keyed only by entity type and path — the shape
|
|
31
|
+
* every other hook in this file takes. `./deep-observe.ts` computes it once
|
|
32
|
+
* per resource and attaches it to the observation as `fieldOwners`. Since
|
|
33
|
+
* chant #1191 ownership no longer prunes anything: a foreign-owned field
|
|
34
|
+
* nobody declared is reported as `undeclared` drift, and the only static
|
|
35
|
+
* subtraction for foreign writes is `K8S_SYSTEM_METADATA_PRUNE_PATTERNS`
|
|
36
|
+
* (core's `managed-fields`), applied below.
|
|
37
|
+
*
|
|
38
|
+
* The *entity-type-agnostic* half of these rules — which fields every
|
|
39
|
+
* Kubernetes API object carries regardless of kind, and the well-known
|
|
40
|
+
* list-map-key ordering conventions (containers/env/volumes/ports) — lives in
|
|
41
|
+
* `@intentius/chant/managed-fields` (chant #1087), because a GCP Config
|
|
42
|
+
* Connector custom resource is a Kubernetes object too and needs the exact
|
|
43
|
+
* same rules without depending on this lexicon's package. What stays here is
|
|
44
|
+
* only what's genuinely k8s-*lexicon*-specific: {@link K8S_SERVICE_DEFAULTS},
|
|
45
|
+
* keyed by chant's own k8s entityType catalog.
|
|
46
|
+
*/
|
|
47
|
+
import { existsSync, readFileSync } from "fs";
|
|
48
|
+
import { join } from "path";
|
|
49
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
50
|
+
import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, K8S_SYSTEM_METADATA_PRUNE_PATTERNS, k8sListMapOrderKey, } from "@intentius/chant/managed-fields";
|
|
51
|
+
import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
52
|
+
import { GENERATED_ONCE_LABEL_KEY } from "./secret-labels.js";
|
|
53
|
+
/**
|
|
54
|
+
* Kubernetes-defaulted fields, per entity type, as index-erased property
|
|
55
|
+
* paths. Subtracted only where source never declared the property
|
|
56
|
+
* (`side === "live" && counterpart === "absent"`) — cdk-real-drift's default
|
|
57
|
+
* subtraction, same convention as the AWS and Azure tables
|
|
58
|
+
* (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`).
|
|
59
|
+
*
|
|
60
|
+
* Sparse and evidence-based rather than derived from the generated schema:
|
|
61
|
+
* the k8s OpenAPI spec this lexicon's codegen consumes
|
|
62
|
+
* (`lexicons/k8s/src/spec/parse.ts`) does not carry a `default` value for
|
|
63
|
+
* these fields the way ARM's schema sometimes does, so "per discovery" is not
|
|
64
|
+
* actually expressible today. Widening this table is additive and needs no
|
|
65
|
+
* contract change.
|
|
66
|
+
*
|
|
67
|
+
* `spec.strategy` is listed whole rather than as `spec.strategy.type`, because
|
|
68
|
+
* pruning only the leaf would still recurse into the object, and a nested default the
|
|
69
|
+
* table does not separately name (`rollingUpdate.maxSurge`/`maxUnavailable`,
|
|
70
|
+
* both `"25%"` when `spec.strategy` is omitted entirely) would leave behind an
|
|
71
|
+
* empty `strategy: {}` — a value distinct from no `strategy` key at all, and
|
|
72
|
+
* itself a spurious "undeclared" finding. Matching the whole node first, before
|
|
73
|
+
* its children are ever visited, drops the wrapper outright when every field
|
|
74
|
+
* under it is exactly at its default.
|
|
75
|
+
*/
|
|
76
|
+
export const K8S_SERVICE_DEFAULTS = {
|
|
77
|
+
"K8s::Apps::Deployment": {
|
|
78
|
+
"spec.strategy": { type: "RollingUpdate", rollingUpdate: { maxSurge: "25%", maxUnavailable: "25%" } },
|
|
79
|
+
"spec.revisionHistoryLimit": 10,
|
|
80
|
+
"spec.progressDeadlineSeconds": 600,
|
|
81
|
+
"spec.template.spec.dnsPolicy": "ClusterFirst",
|
|
82
|
+
"spec.template.spec.restartPolicy": "Always",
|
|
83
|
+
"spec.template.spec.terminationGracePeriodSeconds": 30,
|
|
84
|
+
"spec.template.spec.schedulerName": "default-scheduler",
|
|
85
|
+
// Widened on the CC canonical estate (#1214): a container declaring an
|
|
86
|
+
// httpGet probe, a named port, or nothing about termination gets exactly
|
|
87
|
+
// these from the API server. SSA attributes a server-defaulted field to no
|
|
88
|
+
// manager, so the managed-fields prune deliberately leaves them diffable
|
|
89
|
+
// (see ./deep-observe.ts) — this table is where they subtract.
|
|
90
|
+
"spec.template.spec.containers[].livenessProbe.failureThreshold": 3,
|
|
91
|
+
"spec.template.spec.containers[].livenessProbe.periodSeconds": 10,
|
|
92
|
+
"spec.template.spec.containers[].livenessProbe.successThreshold": 1,
|
|
93
|
+
"spec.template.spec.containers[].livenessProbe.timeoutSeconds": 1,
|
|
94
|
+
"spec.template.spec.containers[].livenessProbe.httpGet.scheme": "HTTP",
|
|
95
|
+
"spec.template.spec.containers[].readinessProbe.failureThreshold": 3,
|
|
96
|
+
"spec.template.spec.containers[].readinessProbe.periodSeconds": 10,
|
|
97
|
+
"spec.template.spec.containers[].readinessProbe.successThreshold": 1,
|
|
98
|
+
"spec.template.spec.containers[].readinessProbe.timeoutSeconds": 1,
|
|
99
|
+
"spec.template.spec.containers[].readinessProbe.httpGet.scheme": "HTTP",
|
|
100
|
+
"spec.template.spec.containers[].ports[].protocol": "TCP",
|
|
101
|
+
"spec.template.spec.containers[].terminationMessagePath": "/dev/termination-log",
|
|
102
|
+
"spec.template.spec.containers[].terminationMessagePolicy": "File",
|
|
103
|
+
"spec.template.spec.securityContext": {},
|
|
104
|
+
},
|
|
105
|
+
"K8s::Core::Service": {
|
|
106
|
+
"spec.sessionAffinity": "None",
|
|
107
|
+
"spec.type": "ClusterIP",
|
|
108
|
+
"spec.internalTrafficPolicy": "Cluster",
|
|
109
|
+
"spec.ipFamilyPolicy": "SingleStack",
|
|
110
|
+
"spec.ports[].protocol": "TCP",
|
|
111
|
+
},
|
|
112
|
+
"K8s::Core::Secret": {
|
|
113
|
+
// The API server defaults an untyped Secret to Opaque (#1830).
|
|
114
|
+
"type": "Opaque",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Server-ASSIGNED fields — populated by the API server with a value that
|
|
119
|
+
* varies per object (an allocated ClusterIP, the resolved IP family list), so
|
|
120
|
+
* unlike {@link K8S_SERVICE_DEFAULTS} there is no fixed value to compare
|
|
121
|
+
* against. Counterpart-gated like the defaults table: a declared `clusterIP`
|
|
122
|
+
* is still compared, only the purely server-filled appearance is noise.
|
|
123
|
+
*/
|
|
124
|
+
export const K8S_SERVER_ASSIGNED_PATTERNS = {
|
|
125
|
+
"K8s::Core::Service": new Set(["spec.clusterIP", "spec.clusterIPs", "spec.ipFamilies"]),
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* chant's own ownership marker, as label paths (#1214 — the k8s edition of
|
|
129
|
+
* azure's #1213 rule). The build stamps `app.kubernetes.io/managed-by` +
|
|
130
|
+
* `chant.intentius.io/{stack,env}` into every manifest it emits, so a managed
|
|
131
|
+
* object always carries them live while the declared *source* the diff
|
|
132
|
+
* compares does not — chant reading its own signature back as drift. The
|
|
133
|
+
* managed-fields prune cannot subtract them: they are in the applied config,
|
|
134
|
+
* so chant's field manager owns them, and chant-owned is always diffable.
|
|
135
|
+
* Counterpart-gated at the call site; managed-by is additionally gated on the
|
|
136
|
+
* value `"chant"`, so `managed-by: helm` appearing out of band still surfaces.
|
|
137
|
+
*/
|
|
138
|
+
const K8S_OWNERSHIP_LABEL_PATTERNS = new Set([
|
|
139
|
+
`metadata.labels.${LABEL_OWNERSHIP_KEYS.stack}`,
|
|
140
|
+
`metadata.labels.${LABEL_OWNERSHIP_KEYS.env}`,
|
|
141
|
+
// The generated-once marker (#1830) is stamped by the secret store at
|
|
142
|
+
// mint time, never by a serializer — chant's own signature, not drift.
|
|
143
|
+
`metadata.labels.${GENERATED_ONCE_LABEL_KEY}`,
|
|
144
|
+
]);
|
|
145
|
+
const K8S_MANAGED_BY_LABEL_PATTERN = `metadata.labels.${LABEL_OWNERSHIP_KEYS.managedBy}`;
|
|
146
|
+
/** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a natural identity field. */
|
|
147
|
+
function canonicalJson(value) {
|
|
148
|
+
return (JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v)
|
|
149
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
150
|
+
: v) ?? "");
|
|
151
|
+
}
|
|
152
|
+
let cachedListMapKeys;
|
|
153
|
+
/**
|
|
154
|
+
* Where the generated table lives: next to this module, resolved from
|
|
155
|
+
* `import.meta.url`. This module ships as ESM (tsx strips types from src/
|
|
156
|
+
* directly), where a bare `require` is not defined — the #1441 regression
|
|
157
|
+
* crashed `lifecycle diff --live` with `require is not defined` on every
|
|
158
|
+
* estate with a Deployment, and azure's apiVersion registry hit the same
|
|
159
|
+
* class of bug in #1581. A plain fs read has no module-system dependency.
|
|
160
|
+
*/
|
|
161
|
+
export function listMapKeyTablePath() {
|
|
162
|
+
return join(moduleSourceDir(import.meta.url), "generated", "list-map-keys.json");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Load the spec-derived table once. A missing or unreadable file degrades to
|
|
166
|
+
* an EMPTY table, which routes every list through `k8sListMapOrderKey`'s
|
|
167
|
+
* hand-written conventions — the same fallback an unlisted property already
|
|
168
|
+
* takes. The empty table is cached by the caller too, so an absent artifact
|
|
169
|
+
* costs one failed read and one warning, not one per array element
|
|
170
|
+
* (chant #1476).
|
|
171
|
+
*
|
|
172
|
+
* Degrading is the right call here, unlike azure's apiVersion registry
|
|
173
|
+
* (#1581) where a silent fallback bypasses correctness-bearing pins. Without
|
|
174
|
+
* the table, drift detection narrows back to the six hardcoded properties:
|
|
175
|
+
* less coverage, never wrong answers. The warning names the regen command so
|
|
176
|
+
* the narrowing is not silent.
|
|
177
|
+
*/
|
|
178
|
+
export function loadListMapKeyTable(path = listMapKeyTablePath()) {
|
|
179
|
+
if (!existsSync(path)) {
|
|
180
|
+
warnMissingTable(path, "not found");
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
warnMissingTable(path, `unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function warnMissingTable(path, reason) {
|
|
192
|
+
console.warn(`k8s: list-map-keys table ${reason} (${path}); list identity falls back to the ` +
|
|
193
|
+
"hand-written conventions in @intentius/chant/managed-fields. Run `npm run generate` " +
|
|
194
|
+
"in lexicons/k8s (dev checkout) or reinstall @intentius/chant-lexicon-k8s.");
|
|
195
|
+
}
|
|
196
|
+
function listMapKeyTable() {
|
|
197
|
+
cachedListMapKeys ?? (cachedListMapKeys = loadListMapKeyTable());
|
|
198
|
+
return cachedListMapKeys;
|
|
199
|
+
}
|
|
200
|
+
/** Test hook: replace (or drop) the cached table so the next lookup reloads it. */
|
|
201
|
+
export function resetListMapKeyTableCache(table) {
|
|
202
|
+
cachedListMapKeys = table;
|
|
203
|
+
}
|
|
204
|
+
/** Last dotted segment of an index-erased path: `spec.template.spec.containers` → `containers`. */
|
|
205
|
+
function lastSegment(pattern) {
|
|
206
|
+
const at = pattern.lastIndexOf(".");
|
|
207
|
+
return at === -1 ? pattern : pattern.slice(at + 1);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Render one key field's value into a sort key fragment. Numbers are
|
|
211
|
+
* zero-padded so the key orders numerically rather than lexicographically —
|
|
212
|
+
* `"443"` would otherwise sort before `"80"`. Cosmetic, since either order
|
|
213
|
+
* canonicalizes the two sides identically, but a human-sensible order is free
|
|
214
|
+
* to have here (the same reasoning `k8sListMapOrderKey` applies to ports).
|
|
215
|
+
*/
|
|
216
|
+
function keyFragment(value) {
|
|
217
|
+
if (typeof value === "number")
|
|
218
|
+
return String(value).padStart(5, "0");
|
|
219
|
+
return typeof value === "string" ? value : canonicalJson(value);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Join a key set's values the way `k8sListMapOrderKey` already does: the bare
|
|
223
|
+
* values, `/`-separated, with no field names.
|
|
224
|
+
*
|
|
225
|
+
* This is not only a sort key. Core renders it as the element's ADDRESS in
|
|
226
|
+
* drift output — `spec.template.spec.containers[#app].image` — so the format
|
|
227
|
+
* is user-visible and load-bearing. Emitting `name=app` here would rewrite
|
|
228
|
+
* every existing drift path. Bare values keep the six properties that were
|
|
229
|
+
* hardcoded byte-identical, and extend to the rest for free: a container port
|
|
230
|
+
* reads `[#08080/TCP]` exactly as before, a condition reads `[#Ready]`.
|
|
231
|
+
*/
|
|
232
|
+
function joinKeyValues(el, keys) {
|
|
233
|
+
return keys.map((k) => keyFragment(el[k])).join("/");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Order an associative list's element by the identity the SPEC declares for it
|
|
237
|
+
* (chant #1441), falling back to `@intentius/chant/managed-fields`'s hand-written
|
|
238
|
+
* conventions for anything the generated table does not cover.
|
|
239
|
+
*
|
|
240
|
+
* The fallback is not vestigial. The table is built from the properties this
|
|
241
|
+
* lexicon's codegen parsed, so it covers core Kubernetes and any CRD that
|
|
242
|
+
* declares the extensions — but a CRD that declares an associative list
|
|
243
|
+
* WITHOUT `x-kubernetes-list-map-keys`, or a Config Connector resource read
|
|
244
|
+
* through GCP's hooks, still lands on the by-name conventions. Keeping both
|
|
245
|
+
* means widening the spec-derived table can never narrow what was already
|
|
246
|
+
* identified.
|
|
247
|
+
*/
|
|
248
|
+
export function schemaListMapOrderKey(element) {
|
|
249
|
+
const candidates = listMapKeyTable()[lastSegment(element.pattern)];
|
|
250
|
+
const el = element.element;
|
|
251
|
+
if (candidates && isRecordLike(el)) {
|
|
252
|
+
for (const keys of candidates) {
|
|
253
|
+
if (!keys.every((k) => el[k] !== undefined))
|
|
254
|
+
continue;
|
|
255
|
+
return joinKeyValues(el, keys);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return k8sListMapOrderKey(element);
|
|
259
|
+
}
|
|
260
|
+
function isRecordLike(value) {
|
|
261
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The paths on a `K8s::Core::Secret` whose values are secret material —
|
|
265
|
+
* `data.<key>` (live and declared base64) and `stringData.<key>` (declared
|
|
266
|
+
* plaintext). Masked on BOTH sides by core's normalization pass, so presence
|
|
267
|
+
* and key names still classify as drift while no value (or value-derived
|
|
268
|
+
* comparison) ever reaches a diff row — #1365 decision 6, the contract #1830's
|
|
269
|
+
* generated-once secrets rely on. Key-name masking alone cannot do this:
|
|
270
|
+
* Secret keys are arbitrary (`app.conf`), not name-shaped.
|
|
271
|
+
*/
|
|
272
|
+
const K8S_SECRET_ENTITY_TYPE = "K8s::Core::Secret";
|
|
273
|
+
const K8S_SECRET_VALUE_PATTERN = /^(?:data|stringData)\./;
|
|
274
|
+
export const k8sDeepNormalizationHooks = {
|
|
275
|
+
mask(node) {
|
|
276
|
+
return node.entityType === K8S_SECRET_ENTITY_TYPE && K8S_SECRET_VALUE_PATTERN.test(node.pattern);
|
|
277
|
+
},
|
|
278
|
+
prune(node) {
|
|
279
|
+
if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern))
|
|
280
|
+
return true;
|
|
281
|
+
if (node.side !== "live" || node.counterpart !== "absent")
|
|
282
|
+
return false;
|
|
283
|
+
// What Kubernetes' own controllers stamp on every object of a kind
|
|
284
|
+
// (rollout counters, selector hashes, client-side-apply bookkeeping) is
|
|
285
|
+
// not drift. This is the static allowlist that stands in for the
|
|
286
|
+
// managed-fields prune since #1191: any *other* foreign-owned field
|
|
287
|
+
// nobody declared — a `kubectl label` from the console — now reaches the
|
|
288
|
+
// diff and reports as `undeclared`, with the accepted baseline as the
|
|
289
|
+
// valve for the ones an estate has decided to live with.
|
|
290
|
+
if (K8S_SYSTEM_METADATA_PRUNE_PATTERNS.has(node.pattern))
|
|
291
|
+
return true;
|
|
292
|
+
// chant's own ownership marker is not drift (see the sets' docs).
|
|
293
|
+
if (K8S_OWNERSHIP_LABEL_PATTERNS.has(node.pattern))
|
|
294
|
+
return true;
|
|
295
|
+
if (node.pattern === K8S_MANAGED_BY_LABEL_PATTERN && node.value === OWNERSHIP_MANAGED_BY_VALUE)
|
|
296
|
+
return true;
|
|
297
|
+
// A server-assigned value nobody declared is allocation, not drift.
|
|
298
|
+
if (K8S_SERVER_ASSIGNED_PATTERNS[node.entityType]?.has(node.pattern))
|
|
299
|
+
return true;
|
|
300
|
+
const defaults = K8S_SERVICE_DEFAULTS[node.entityType];
|
|
301
|
+
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern))
|
|
302
|
+
return false;
|
|
303
|
+
return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
|
|
304
|
+
},
|
|
305
|
+
orderKey: schemaListMapOrderKey,
|
|
306
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes deep observation (#1076) — the k8s row of the deep-observe
|
|
3
|
+
* contract (#1014), and the epic's (#1073) thesis made real: chant compiled
|
|
4
|
+
* the manifest, so it knows which fields it authored; the API server records,
|
|
5
|
+
* per field, which manager last wrote it (`metadata.managedFields`, chant
|
|
6
|
+
* #1075/#1178). Intersecting those two makes Kubernetes drift *derived*
|
|
7
|
+
* rather than the hand-maintained `ignoreDifferences` list every Argo user
|
|
8
|
+
* keeps.
|
|
9
|
+
*
|
|
10
|
+
* ## What managedFields decides, and what it does not
|
|
11
|
+
*
|
|
12
|
+
* The AWS and Azure rows both prune by a **static, entityType-keyed** table
|
|
13
|
+
* (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`): an ARN always looks like
|
|
14
|
+
* an ARN, `provisioningState` is always server-populated. Neither needs the
|
|
15
|
+
* specific live object in hand — it is exactly
|
|
16
|
+
* what `./deep-observe-hooks.ts`'s `k8sDeepNormalizationHooks` is, and it
|
|
17
|
+
* covers Kubernetes' *equivalent* static noise (`status`,
|
|
18
|
+
* `metadata.{uid,resourceVersion,generation,creationTimestamp}`, a handful of
|
|
19
|
+
* server-defaulted pod-spec fields, and the annotations and labels
|
|
20
|
+
* Kubernetes' own controllers stamp on every object of a kind —
|
|
21
|
+
* `K8S_SYSTEM_METADATA_PRUNE_PATTERNS` in `@intentius/chant/managed-fields`).
|
|
22
|
+
*
|
|
23
|
+
* managedFields is a different kind of fact. Which manager owns
|
|
24
|
+
* `spec.replicas` on *this* Deployment differs between two Deployments of
|
|
25
|
+
* the same type, and the declared tree never carries it at all (chant's
|
|
26
|
+
* source has no `managedFields` key to normalize). It has to be resolved once
|
|
27
|
+
* per resource, here, against the object in hand.
|
|
28
|
+
*
|
|
29
|
+
* Until chant #1191 that resolution *pruned*: a foreign-owned path nobody
|
|
30
|
+
* declared was dropped before the tree reached core, on the theory that it
|
|
31
|
+
* was controller noise. It silenced exactly the case the deep read exists
|
|
32
|
+
* for — `kubectl label deploy web team=platform` is foreign-owned
|
|
33
|
+
* (`kubectl-edit`) and undeclared, and never appeared in fieldDrift. The
|
|
34
|
+
* policy now (recorded on #1202, refined by #2160) is:
|
|
35
|
+
*
|
|
36
|
+
* 1. **Chant-owned** (`chant`, `chant:<stack>` — chant #1075, matched on the
|
|
37
|
+
* family so a stack rename does not stop recognizing its own history) is
|
|
38
|
+
* always diffable.
|
|
39
|
+
* 2. **Declared, whoever owns it live** (a contested field) is always
|
|
40
|
+
* diffable: chant's source is a statement of intent independent of which
|
|
41
|
+
* write currently holds the field, and a foreign write that overrides a
|
|
42
|
+
* value chant's manifest asks for is the thing `lifecycle diff --live`
|
|
43
|
+
* exists to surface. It reports as `changed`.
|
|
44
|
+
* 3. **Undeclared** — a path outside the declaration's claimed-field set
|
|
45
|
+
* (`@intentius/chant/claimed-fields`) — is reported as unclaimed and
|
|
46
|
+
* is not drift at all. Before #2160 it was `undeclared` drift that the
|
|
47
|
+
* accepted baseline had to absorb one path at a time; the declaration
|
|
48
|
+
* answers on the first read instead, so an HPA that owns `spec.replicas`
|
|
49
|
+
* on a Deployment whose source is silent about replicas is quiet with
|
|
50
|
+
* nothing recorded anywhere.
|
|
51
|
+
*
|
|
52
|
+
* So ownership decides nothing about *whether* a path is compared, and since
|
|
53
|
+
* #2160 it no longer decides the verdict either — the declaration does. What
|
|
54
|
+
* it contributes is `fieldOwners` (#1189), and that is worth more here than
|
|
55
|
+
* anywhere else: on every other substrate a held field can only be reported
|
|
56
|
+
* as "not chant's", while here it is reported as `hpa-controller`'s or
|
|
57
|
+
* `kubectl-edit`'s by name, which is the difference between a controller
|
|
58
|
+
* doing its job and somebody bypassing the pipeline. Core takes the manager
|
|
59
|
+
* where this reader supplies one and falls back to the claim where it does
|
|
60
|
+
* not (a path inside a key-addressed list, which no `fieldsV1` entry names in
|
|
61
|
+
* that form). The only fields subtracted because a foreign manager wrote them
|
|
62
|
+
* are the well-known system ones on the static allowlist.
|
|
63
|
+
*
|
|
64
|
+
* ## Resolving a managedFields entry against a live array
|
|
65
|
+
*
|
|
66
|
+
* `metadata.managedFields[].fieldsV1` encodes a list item three ways —
|
|
67
|
+
* `k:{"name":"web"}` (by key), `v:"blue"` (by value), `i:3` (by index) — and
|
|
68
|
+
* chant #1178's `managed-fields.ts` renders all three to a path *string*
|
|
69
|
+
* (`.spec.containers[name="web"]`) for display. Parsing that string back
|
|
70
|
+
* would be ambiguous in general (a label key may itself contain `.`, e.g.
|
|
71
|
+
* `app.kubernetes.io/name`, indistinguishable in the rendered form from
|
|
72
|
+
* nested fields), so this reader does not parse strings at all: it walks the
|
|
73
|
+
* *structured* `fieldsV1` tree directly (`walkOwnership`, below), matching
|
|
74
|
+
* each `k:`/`v:` entry against the actual live array to find the concrete
|
|
75
|
+
* index, and against the actual declared array (by the same key or value) to
|
|
76
|
+
* answer question 3 for that element specifically — a declared container
|
|
77
|
+
* named `app` at index 0 is still "declared" if a sidecar gets injected in
|
|
78
|
+
* front of it and shifts it to live index 1.
|
|
79
|
+
*
|
|
80
|
+
* ## What is deliberately not modeled
|
|
81
|
+
*
|
|
82
|
+
* A manager's `.` entry (SSA's "I own this element's presence") on a
|
|
83
|
+
* container that also sets no scalar sub-fields (unusual — a real apply
|
|
84
|
+
* setting a container almost always sets `image`/`name` too) would not confer
|
|
85
|
+
* ownership onto a sibling field nobody's `fieldsV1` entry lists explicitly.
|
|
86
|
+
* A path absent from every manager's fields simply has no `owner` on its
|
|
87
|
+
* drift line; it is compared like any other.
|
|
88
|
+
*
|
|
89
|
+
* ## `buildOwnershipSets` lives in core now
|
|
90
|
+
*
|
|
91
|
+
* The ownership walk itself (`walkOwnership`, resolving a `fieldsV1` tree
|
|
92
|
+
* against the live and declared trees) has nothing k8s-*lexicon*-specific
|
|
93
|
+
* about it — it is generic Kubernetes SSA machinery. Chant #1087 (GCP's row,
|
|
94
|
+
* reusing this one, since a Config Connector CR is a Kubernetes object too)
|
|
95
|
+
* moved it to `@intentius/chant/managed-fields` rather than have gcp depend
|
|
96
|
+
* on this lexicon's package, the same reason chant #1100's
|
|
97
|
+
* `resolveClusterTarget` lives in core. This module re-exports
|
|
98
|
+
* `buildOwnershipSets`/`OwnershipSets` from there so nothing here changes for
|
|
99
|
+
* an existing importer of this file.
|
|
100
|
+
*/
|
|
101
|
+
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
102
|
+
import { unobservedAll } from "@intentius/chant/observation";
|
|
103
|
+
import { hasOwnershipMarker, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
104
|
+
import { buildOwnershipSets } from "@intentius/chant/managed-fields";
|
|
105
|
+
import { defaultK8sConnector } from "./api/connect.js";
|
|
106
|
+
import { classifyApiFailure, isMissingClientPackage, isWholeLexiconFailure, MISSING_CLIENT_DETAIL, } from "./api/classify.js";
|
|
107
|
+
import { operationFor } from "./api/operation-surface.js";
|
|
108
|
+
import { k8sDeepNormalizationHooks } from "./deep-observe-hooks.js";
|
|
109
|
+
import { observeReceiptRowsDeep, receiptRowsFor } from "./receipt-store.js";
|
|
110
|
+
// Re-exported so a dynamic importer of this module (plugin.ts's
|
|
111
|
+
// `observeResourcesDeep`, a test) can get the reader and its hooks from one
|
|
112
|
+
// place, the same shape the AWS and Azure deep-observe.ts modules offer.
|
|
113
|
+
// `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
|
|
114
|
+
// separately, directly from `./deep-observe-hooks` — that file has no
|
|
115
|
+
// dependency on `@intentius/chant-k8s-client`, so it is safe to import
|
|
116
|
+
// statically; this module is not (see the module doc).
|
|
117
|
+
export { k8sDeepNormalizationHooks, buildOwnershipSets };
|
|
118
|
+
/** The live object minus the envelope fields that live outside `properties` on {@link DeepResourceObservation} (mirrors `type`/`physicalId`). */
|
|
119
|
+
function propertiesTreeOf(obj) {
|
|
120
|
+
const { apiVersion: _apiVersion, kind: _kind, ...rest } = obj;
|
|
121
|
+
return rest;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read the live property tree for each declared entity, pruning by
|
|
125
|
+
* `metadata.managedFields` (see the module doc). Reuses the same connector,
|
|
126
|
+
* discovery/caching client and tri-state failure classification as the thin
|
|
127
|
+
* read (`./describe-resources.ts`) — the binding check (chant #1100/#1155)
|
|
128
|
+
* still refuses before any resource is touched, and a connect failure still
|
|
129
|
+
* becomes NOT-OBSERVED for every declared entity rather than an empty result.
|
|
130
|
+
*/
|
|
131
|
+
export async function observeResourcesDeepK8s(options, connect = defaultK8sConnector) {
|
|
132
|
+
const { managedFieldsOf, isChantFieldManager } = await import("@intentius/chant-k8s-client");
|
|
133
|
+
// Effect receipts (#2074) are read by their own leg at the end: they carry
|
|
134
|
+
// no declared props, so every live path would be an unclaimed field (#2160)
|
|
135
|
+
// and their staleness is an `effect` row from the plan (#1832), never
|
|
136
|
+
// property drift. Reading them here keeps the deep read from calling a
|
|
137
|
+
// declared entity a hole; contributing an empty tree keeps it from calling
|
|
138
|
+
// one drift.
|
|
139
|
+
const receiptRows = receiptRowsFor(options.entityNames, options.buildOutput);
|
|
140
|
+
const declared = [...options.entities]
|
|
141
|
+
.filter(([entityName]) => !receiptRows.has(entityName))
|
|
142
|
+
.map(([entityName, entity]) => ({
|
|
143
|
+
entityName,
|
|
144
|
+
entityType: entity.entityType,
|
|
145
|
+
props: entity.props,
|
|
146
|
+
}));
|
|
147
|
+
const everyName = [...declared.map((d) => d.entityName), ...receiptRows.keys()];
|
|
148
|
+
let client;
|
|
149
|
+
try {
|
|
150
|
+
({ client } = await connect({ environment: options.environment, cwd: options.cwd }));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (isMissingClientPackage(err)) {
|
|
154
|
+
return deepObservation({}, unobservedAll(everyName, "read-failed", MISSING_CLIENT_DETAIL, options.entities));
|
|
155
|
+
}
|
|
156
|
+
if (isWholeLexiconFailure(err)) {
|
|
157
|
+
const outcome = classifyApiFailure(err);
|
|
158
|
+
return deepObservation({}, unobservedAll(everyName, outcome.kind === "unobserved" ? outcome.reason : "read-failed", outcome.kind === "unobserved" ? outcome.detail : undefined, options.entities));
|
|
159
|
+
}
|
|
160
|
+
// A cluster-binding mismatch (chant #1100) — refuse loudly, same as the
|
|
161
|
+
// thin path; core turns the throw into NOT-OBSERVED for every entity.
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
const resources = {};
|
|
165
|
+
const unobserved = {};
|
|
166
|
+
await client.concurrently(declared, async ({ entityName, entityType, props }) => {
|
|
167
|
+
const operation = operationFor(entityType);
|
|
168
|
+
if (!operation) {
|
|
169
|
+
unobserved[entityName] = {
|
|
170
|
+
type: entityType,
|
|
171
|
+
reason: "unsupported-kind",
|
|
172
|
+
detail: `no generated operation surface for ${entityType} — run \`chant generate\` in the k8s lexicon, or declare the CRD as a codegen source`,
|
|
173
|
+
};
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const metadata = props.metadata;
|
|
177
|
+
const name = metadata?.name;
|
|
178
|
+
if (!name) {
|
|
179
|
+
unobserved[entityName] = {
|
|
180
|
+
type: entityType,
|
|
181
|
+
reason: "read-failed",
|
|
182
|
+
detail: "declared entity has no metadata.name to query by",
|
|
183
|
+
};
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const obj = await client.read({
|
|
188
|
+
apiVersion: operation.apiVersion,
|
|
189
|
+
kind: operation.kind,
|
|
190
|
+
name,
|
|
191
|
+
...(metadata?.namespace ? { namespace: metadata.namespace } : {}),
|
|
192
|
+
});
|
|
193
|
+
if (options.owned && !hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS)) {
|
|
194
|
+
unobserved[entityName] = {
|
|
195
|
+
type: entityType,
|
|
196
|
+
reason: "filtered",
|
|
197
|
+
detail: "live object carries no chant ownership marker and --owned was requested",
|
|
198
|
+
};
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const liveRoot = propertiesTreeOf(obj);
|
|
202
|
+
const sets = buildOwnershipSets(managedFieldsOf(obj), liveRoot, props, isChantFieldManager);
|
|
203
|
+
resources[entityName] = {
|
|
204
|
+
type: entityType,
|
|
205
|
+
physicalId: obj.metadata?.uid,
|
|
206
|
+
properties: normalizeDeepProperties(liveRoot, {
|
|
207
|
+
entityType,
|
|
208
|
+
side: "live",
|
|
209
|
+
hooks: k8sDeepNormalizationHooks,
|
|
210
|
+
}),
|
|
211
|
+
// Who owns each path (#1189). Nothing is pruned by ownership any more
|
|
212
|
+
// (#1191), so a foreign-owned undeclared field still reaches the diff
|
|
213
|
+
// — as a held field since #2160 — and naming the manager is the whole
|
|
214
|
+
// question there: an operator needs to tell `hpa-controller` doing its
|
|
215
|
+
// job from somebody running `kubectl edit`.
|
|
216
|
+
...(sets.owners.size > 0 ? { fieldOwners: Object.fromEntries(sets.owners) } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const outcome = classifyApiFailure(err);
|
|
221
|
+
if (outcome.kind === "unobserved") {
|
|
222
|
+
unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
|
|
223
|
+
}
|
|
224
|
+
// `absent` records nothing — the thin read already reports the
|
|
225
|
+
// non-existence; restating it here would turn one finding into two.
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (receiptRows.size > 0) {
|
|
229
|
+
const receiptObs = await observeReceiptRowsDeep(client, receiptRows);
|
|
230
|
+
Object.assign(resources, receiptObs.resources);
|
|
231
|
+
Object.assign(unobserved, receiptObs.unobserved);
|
|
232
|
+
}
|
|
233
|
+
return deepObservation(resources, unobserved);
|
|
234
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Labels & Annotations — declares project-wide labels/annotations
|
|
3
|
+
* for all Kubernetes resources.
|
|
4
|
+
*
|
|
5
|
+
* When a project exports a `defaultLabels(...)` or `defaultAnnotations(...)`
|
|
6
|
+
* declaration, the serializer automatically injects them into every resource's
|
|
7
|
+
* metadata at synthesis time. Explicit labels/annotations on individual
|
|
8
|
+
* resources take precedence.
|
|
9
|
+
*/
|
|
10
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
11
|
+
/**
|
|
12
|
+
* Marker symbol for default labels identification.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_LABELS_MARKER = Symbol.for("chant.k8s.defaultLabels");
|
|
15
|
+
/**
|
|
16
|
+
* Marker symbol for default annotations identification.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_ANNOTATIONS_MARKER = Symbol.for("chant.k8s.defaultAnnotations");
|
|
19
|
+
/**
|
|
20
|
+
* Type guard for DefaultLabels.
|
|
21
|
+
*/
|
|
22
|
+
export function isDefaultLabels(value) {
|
|
23
|
+
return (typeof value === "object" &&
|
|
24
|
+
value !== null &&
|
|
25
|
+
DEFAULT_LABELS_MARKER in value &&
|
|
26
|
+
value[DEFAULT_LABELS_MARKER] === true);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Type guard for DefaultAnnotations.
|
|
30
|
+
*/
|
|
31
|
+
export function isDefaultAnnotations(value) {
|
|
32
|
+
return (typeof value === "object" &&
|
|
33
|
+
value !== null &&
|
|
34
|
+
DEFAULT_ANNOTATIONS_MARKER in value &&
|
|
35
|
+
value[DEFAULT_ANNOTATIONS_MARKER] === true);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Declare project-wide default labels for all Kubernetes resources.
|
|
39
|
+
*
|
|
40
|
+
* Labels are injected at synthesis time into every resource's
|
|
41
|
+
* `metadata.labels`. If a resource has an explicit label with the
|
|
42
|
+
* same key, the explicit value wins.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { defaultLabels } from "@intentius/chant-lexicon-k8s";
|
|
47
|
+
*
|
|
48
|
+
* export const labels = defaultLabels({
|
|
49
|
+
* "app.kubernetes.io/managed-by": "chant",
|
|
50
|
+
* "app.kubernetes.io/part-of": "my-app",
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function defaultLabels(labels) {
|
|
55
|
+
return {
|
|
56
|
+
[DEFAULT_LABELS_MARKER]: true,
|
|
57
|
+
[DECLARABLE_MARKER]: true,
|
|
58
|
+
lexicon: "k8s",
|
|
59
|
+
entityType: "chant:k8s:defaultLabels",
|
|
60
|
+
labels,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Declare project-wide default annotations for all Kubernetes resources.
|
|
65
|
+
*
|
|
66
|
+
* Annotations are injected at synthesis time into every resource's
|
|
67
|
+
* `metadata.annotations`. If a resource has an explicit annotation with
|
|
68
|
+
* the same key, the explicit value wins.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { defaultAnnotations } from "@intentius/chant-lexicon-k8s";
|
|
73
|
+
*
|
|
74
|
+
* export const annotations = defaultAnnotations({
|
|
75
|
+
* "app.kubernetes.io/managed-by": "chant",
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function defaultAnnotations(annotations) {
|
|
80
|
+
return {
|
|
81
|
+
[DEFAULT_ANNOTATIONS_MARKER]: true,
|
|
82
|
+
[DECLARABLE_MARKER]: true,
|
|
83
|
+
lexicon: "k8s",
|
|
84
|
+
entityType: "chant:k8s:defaultAnnotations",
|
|
85
|
+
annotations,
|
|
86
|
+
};
|
|
87
|
+
}
|