@intentius/chant-lexicon-gcp 0.37.2 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/read-client.d.ts +71 -0
- package/dist/api/read-client.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +41 -30
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +24 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +65 -39
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/floci-gcp.d.ts +12 -1
- package/dist/op/activities/floci-gcp.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +129 -0
- package/src/deep-observe-hooks.ts +112 -60
- package/src/deep-observe.test.ts +124 -478
- package/src/deep-observe.ts +94 -74
- package/src/describe-resources.test.ts +119 -206
- package/src/describe-resources.ts +153 -111
- package/src/lifecycle-integration.test.ts +32 -18
- package/src/op/activities/floci-gcp.test.ts +1 -1
- package/src/op/activities/floci-gcp.ts +22 -5
- package/src/plugin.ts +4 -0
|
@@ -1,79 +1,131 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GCP deep-observation noise rules (#1209) — for REST payloads, not Config
|
|
3
|
+
* Connector objects.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* the k8s lexicon's rules into `lexicons/k8s/src/deep-observe-hooks.ts`: this
|
|
6
|
-
* file must be safe to import from `plugin.ts` at module load time, because
|
|
7
|
-
* `LexiconPlugin.deepNormalizationHooks` is plain data core reads to
|
|
8
|
-
* normalize the *declared* tree — the half of the contract that runs whether
|
|
9
|
-
* or not a cluster is ever touched (`lifecycle diff` without `--live`,
|
|
10
|
-
* `chant build`, tests that only exercise normalization). `./deep-observe.ts`
|
|
11
|
-
* imports `./describe-resources.ts` for the live kubectl transport, which
|
|
12
|
-
* pulls in `node:child_process`; nothing this file exports may pull that in,
|
|
13
|
-
* directly or transitively, so `chant build` never resolves a process-
|
|
14
|
-
* spawning module just to normalize a declared tree.
|
|
5
|
+
* ## What this replaced, and why it had to
|
|
15
6
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
7
|
+
* Until #1209 the deep reader read Config Connector custom resources through
|
|
8
|
+
* kubectl, and suppressed noise two ways: this module's static rules (the k8s
|
|
9
|
+
* object envelope plus CNRM's own observed-state annotations) and, in
|
|
10
|
+
* `./deep-observe.ts`, a per-resource prune driven by `metadata.managedFields`
|
|
11
|
+
* — Kubernetes records which manager owns which field, so everything the CNRM
|
|
12
|
+
* controller owned was subtracted before the diff ran.
|
|
13
|
+
*
|
|
14
|
+
* Reading GCP's own REST APIs removes both. **A GCP payload never says who
|
|
15
|
+
* wrote a field**, so there is no managed-fields attribution to prune by, and
|
|
16
|
+
* it is not a Kubernetes object, so the k8s envelope and CNRM annotation rules
|
|
17
|
+
* match nothing in it. Audited every rule that was here: all of them were
|
|
18
|
+
* CNRM-shaped, none applies.
|
|
19
|
+
*
|
|
20
|
+
* So the noise story is re-answered rather than re-pointed, in the shape the
|
|
21
|
+
* other two cloud lexicons already use — a static table naming what the
|
|
22
|
+
* provider populates. It is hand-maintained, which is the honest cost: a
|
|
23
|
+
* server-set field nobody has listed reads as drift until it is. That is
|
|
24
|
+
* visible and fixable, where an over-broad rule silently hides real drift.
|
|
21
25
|
*/
|
|
22
|
-
|
|
23
|
-
import type { DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
24
|
-
import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
|
|
26
|
+
import type { DeepNode, DeepNormalizationHooks } from "@intentius/chant/deep-observation";
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
* `cnrm-controller-manager` after reconciling, mirroring GCP-side state the
|
|
29
|
-
* CRD schema has no field for (e.g. secret version pins). Never authored by
|
|
30
|
-
* a chant manifest, so pruning them unconditionally cannot hide declared
|
|
31
|
-
* config the way blindly pruning every `cnrm.cloud.google.com/*` annotation
|
|
32
|
-
* would — several of that family (`state-into-spec`, `deletion-policy`,
|
|
33
|
-
* `management-conflict-prevention-policy`) are themselves user-authored
|
|
34
|
-
* configuration, not observed state, and pruning those would hide genuine
|
|
35
|
-
* drift on them.
|
|
29
|
+
* Fields GCP populates on essentially anything, wherever they appear.
|
|
36
30
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* Most CNRM-controller noise doesn't need this list at all — it's already
|
|
42
|
-
* pruned by the reused managed-fields ownership rule in `./deep-observe.ts`,
|
|
43
|
-
* since `cnrm-controller-manager` is a foreign, undeclared owner of whatever
|
|
44
|
-
* it touches. This list only covers the narrow, evidence-based case worth
|
|
45
|
-
* pruning even were managedFields attribution ever missing for it.
|
|
31
|
+
* Pruned on BOTH sides and regardless of whether source declared them: these
|
|
32
|
+
* are server-assigned identity and bookkeeping, and a user who writes one is
|
|
33
|
+
* writing something the API will overwrite anyway.
|
|
46
34
|
*/
|
|
47
|
-
const
|
|
48
|
-
"
|
|
35
|
+
export const GCP_READ_ONLY_NAMES: ReadonlySet<string> = new Set([
|
|
36
|
+
"etag",
|
|
37
|
+
"selfLink",
|
|
38
|
+
"id",
|
|
39
|
+
"projectNumber",
|
|
40
|
+
"timeCreated",
|
|
41
|
+
"updated",
|
|
42
|
+
"createTime",
|
|
43
|
+
"updateTime",
|
|
44
|
+
"generation",
|
|
45
|
+
"metageneration",
|
|
46
|
+
"uid",
|
|
47
|
+
"state",
|
|
48
|
+
"reconciling",
|
|
49
|
+
"observedGeneration",
|
|
49
50
|
]);
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Per-kind values GCP fills in when the request omits them.
|
|
54
|
+
*
|
|
55
|
+
* Unlike {@link GCP_READ_ONLY_NAMES} these are only noise where **source never
|
|
56
|
+
* declared the property** — a default somebody explicitly wrote is a fact worth
|
|
57
|
+
* diffing, and pruning it would hide a real change away from it. The
|
|
58
|
+
* `counterpart === "absent"` gate below is what enforces that, and it is the
|
|
59
|
+
* same rule the AWS and Azure hooks apply.
|
|
60
|
+
*/
|
|
61
|
+
export const GCP_SERVICE_DEFAULTS: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|
62
|
+
StorageBucket: {
|
|
63
|
+
storageClass: "STANDARD",
|
|
64
|
+
locationType: "multi-region",
|
|
65
|
+
"iamConfiguration.publicAccessPrevention": "inherited",
|
|
66
|
+
"iamConfiguration.uniformBucketLevelAccess.enabled": false,
|
|
67
|
+
"softDeletePolicy.retentionDurationSeconds": "604800",
|
|
68
|
+
kind: "storage#bucket",
|
|
69
|
+
},
|
|
70
|
+
PubSubTopic: {
|
|
71
|
+
"messageStoragePolicy.enforceInTransit": false,
|
|
72
|
+
},
|
|
73
|
+
PubSubSubscription: {
|
|
74
|
+
ackDeadlineSeconds: 10,
|
|
75
|
+
"expirationPolicy.ttl": "2678400s",
|
|
76
|
+
enableMessageOrdering: false,
|
|
77
|
+
"retryPolicy.minimumBackoff": "10s",
|
|
78
|
+
},
|
|
79
|
+
SecretManagerSecret: {
|
|
80
|
+
etag: undefined, // covered by READ_ONLY; listed for readers looking here first
|
|
81
|
+
},
|
|
82
|
+
RunService: {
|
|
83
|
+
ingress: "INGRESS_TRAFFIC_ALL",
|
|
84
|
+
launchStage: "GA",
|
|
85
|
+
},
|
|
86
|
+
IAMServiceAccount: {
|
|
87
|
+
disabled: false,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** chant's own ownership labels, as the applier stamps them. */
|
|
92
|
+
const CHANT_OWNERSHIP_LABEL_PATTERNS: ReadonlySet<string> = new Set([
|
|
93
|
+
"metadata.labels.managed-by",
|
|
94
|
+
"metadata.labels.chant-stack",
|
|
95
|
+
"metadata.labels.chant-env",
|
|
96
|
+
]);
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
/** Last dotted segment of a path pattern — `spec.foo.etag` -> `etag`. */
|
|
99
|
+
function lastSegment(pattern: string): string {
|
|
100
|
+
const i = pattern.lastIndexOf(".");
|
|
101
|
+
return i === -1 ? pattern : pattern.slice(i + 1);
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
/**
|
|
59
|
-
* GCP's static deep-observation noise rules
|
|
60
|
-
*
|
|
61
|
-
* exactly like the other three rows, and layered under the per-resource
|
|
62
|
-
* managed-fields prune in `./deep-observe.ts` for the live tree.
|
|
63
|
-
*
|
|
64
|
-
* A Config Connector custom resource *is* a Kubernetes object (see
|
|
65
|
-
* `./deep-observe.ts`'s module doc), so the generic envelope
|
|
66
|
-
* (`K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS`) and Kubernetes' own list-map-key
|
|
67
|
-
* ordering conventions (`k8sListMapOrderKey` — needed because some CNRM kinds
|
|
68
|
-
* embed genuinely k8s-shaped substructures, e.g. Cloud Run's `RunService`)
|
|
69
|
-
* are reused verbatim from `@intentius/chant/managed-fields` rather than
|
|
70
|
-
* restated. The one GCP-specific rule is CNRM's own observed-state
|
|
71
|
-
* annotation noise.
|
|
105
|
+
* GCP's static deep-observation noise rules, applied by core to the declared
|
|
106
|
+
* tree and the live tree alike so the two are compared in the same shape.
|
|
72
107
|
*/
|
|
73
108
|
export const gcpDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
74
109
|
prune(node: DeepNode): boolean {
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
// Server-assigned, wherever it appears, on either side.
|
|
111
|
+
if (GCP_READ_ONLY_NAMES.has(lastSegment(node.pattern))) return true;
|
|
112
|
+
|
|
113
|
+
// chant's own ownership marker is not drift. The applier stamps these, so
|
|
114
|
+
// reporting them back is chant showing its own signature to itself — the
|
|
115
|
+
// same correction #1301 made for AWS after the emulator began applying tags.
|
|
116
|
+
if (CHANT_OWNERSHIP_LABEL_PATTERNS.has(node.pattern)) return true;
|
|
117
|
+
|
|
118
|
+
// A provider default is noise only where source never declared the
|
|
119
|
+
// property. `counterpart` is a tri-state; only `absent` licenses this.
|
|
120
|
+
if (node.side !== "live" || node.counterpart !== "absent") return false;
|
|
121
|
+
const kindDefaults = GCP_SERVICE_DEFAULTS[shortKind(node.entityType)];
|
|
122
|
+
if (!kindDefaults) return false;
|
|
123
|
+
return Object.prototype.hasOwnProperty.call(kindDefaults, node.pattern) && kindDefaults[node.pattern] === node.value;
|
|
77
124
|
},
|
|
78
|
-
orderKey: k8sListMapOrderKey,
|
|
79
125
|
};
|
|
126
|
+
|
|
127
|
+
/** `GCP::Storage::Bucket` -> `StorageBucket`, the key `GCP_SERVICE_DEFAULTS` uses. */
|
|
128
|
+
function shortKind(entityType: string): string {
|
|
129
|
+
const parts = entityType.split("::");
|
|
130
|
+
return parts.length === 3 ? `${parts[1]}${parts[2]}` : entityType;
|
|
131
|
+
}
|