@intentius/chant-lexicon-gcp 0.37.2 → 0.39.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/composites/cloud-function.d.ts +1 -1
- package/dist/composites/cloud-function.d.ts.map +1 -1
- package/dist/composites/cloud-run-service.d.ts +1 -1
- package/dist/composites/cloud-run-service.d.ts.map +1 -1
- package/dist/composites/cloud-sql-instance.d.ts +5 -1
- package/dist/composites/cloud-sql-instance.d.ts.map +1 -1
- package/dist/composites/gcs-bucket.d.ts +3 -1
- package/dist/composites/gcs-bucket.d.ts.map +1 -1
- package/dist/composites/gke-cluster.d.ts +5 -1
- package/dist/composites/gke-cluster.d.ts.map +1 -1
- package/dist/composites/managed-certificate.d.ts +1 -1
- package/dist/composites/managed-certificate.d.ts.map +1 -1
- package/dist/composites/memorystore-redis.d.ts +3 -1
- package/dist/composites/memorystore-redis.d.ts.map +1 -1
- package/dist/composites/private-service.d.ts +1 -1
- package/dist/composites/private-service.d.ts.map +1 -1
- package/dist/composites/pubsub-pipeline.d.ts +1 -1
- package/dist/composites/pubsub-pipeline.d.ts.map +1 -1
- package/dist/composites/secure-project.d.ts +1 -1
- package/dist/composites/secure-project.d.ts.map +1 -1
- package/dist/composites/vpc-network.d.ts +1 -1
- package/dist/composites/vpc-network.d.ts.map +1 -1
- 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 +27 -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/composites/cloud-function.ts +1 -1
- package/src/composites/cloud-run-service.ts +1 -1
- package/src/composites/cloud-sql-instance.ts +1 -1
- package/src/composites/gcs-bucket.ts +1 -1
- package/src/composites/gke-cluster.ts +1 -1
- package/src/composites/managed-certificate.ts +1 -1
- package/src/composites/memorystore-redis.ts +1 -1
- package/src/composites/private-service.ts +1 -1
- package/src/composites/pubsub-pipeline.ts +1 -1
- package/src/composites/secure-project.ts +1 -1
- package/src/composites/vpc-network.ts +1 -1
- 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 +26 -1
- package/src/op/activities/floci-gcp.ts +37 -5
- package/src/plugin.ts +4 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
|
|
1
2
|
export interface FlociGcpUpArgs {
|
|
2
3
|
/** Container name. Default: `chant-floci-gcp`. */
|
|
3
4
|
name?: string;
|
|
4
5
|
/** Host port mapped to the emulator's `:4588`. Default: `4588`. */
|
|
5
6
|
port?: number;
|
|
6
|
-
/** Image. Default: `floci/floci-gcp
|
|
7
|
+
/** Image. Default: the pinned `floci/floci-gcp` tag. */
|
|
7
8
|
image?: string;
|
|
8
9
|
/** Readiness timeout in ms. Default: `60000`. */
|
|
9
10
|
timeoutMs?: number;
|
|
@@ -14,6 +15,31 @@ export interface FlociGcpDownArgs {
|
|
|
14
15
|
/** Container name to remove. Default: `chant-floci-gcp`. */
|
|
15
16
|
name?: string;
|
|
16
17
|
}
|
|
18
|
+
export declare const FLOCI_GCP_SPEC: EmulatorSpec;
|
|
19
|
+
/**
|
|
20
|
+
* The gcp plugin's emulator capability (#1345, corrected in #1431).
|
|
21
|
+
*
|
|
22
|
+
* The apply and read paths reach the emulator differently, and the capability
|
|
23
|
+
* exists for the one that cannot be told where to look:
|
|
24
|
+
*
|
|
25
|
+
* - **apply** — `gcpApply` takes an explicit `endpoint` argument. Nothing to
|
|
26
|
+
* inject; the caller already holds it.
|
|
27
|
+
* - **read** — `describeResources` and `observeResourcesDeep` take it from
|
|
28
|
+
* `GCP_ENDPOINT_URL` and nothing else (`../../describe-resources.ts`,
|
|
29
|
+
* `../../deep-observe.ts`). A reader given no variable talks to real GCP.
|
|
30
|
+
*
|
|
31
|
+
* `env` was empty because when #1345 declared this capability the second bullet
|
|
32
|
+
* was not yet true: GCP was observed through Config Connector over kubectl, so
|
|
33
|
+
* there genuinely was no variable to inject. #1209 moved observation onto the
|
|
34
|
+
* applier's own direct-REST transport about two hours later, and nothing
|
|
35
|
+
* re-read this comment. The result was an emulator that booted, reported its
|
|
36
|
+
* endpoint, and left every `--live` read pointed at production.
|
|
37
|
+
*
|
|
38
|
+
* Injecting it makes `chant emulator up --lexicon gcp` mean the same thing for
|
|
39
|
+
* gcp that it already means for aws and azure: the estate's reads land on the
|
|
40
|
+
* emulator you just started.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FLOCI_GCP_EMULATOR: EmulatorCapability;
|
|
17
43
|
export declare const flociGcpExistsCommand: (name: string) => string;
|
|
18
44
|
export declare const flociGcpRmCommand: (name: string) => string;
|
|
19
45
|
export declare const flociGcpHealthUrl: (port: number) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floci-gcp.d.ts","sourceRoot":"","sources":["../../../src/op/activities/floci-gcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"floci-gcp.d.ts","sourceRoot":"","sources":["../../../src/op/activities/floci-gcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEpG,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAKD,eAAO,MAAM,cAAc,EAAE,YAM5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,EAAE,kBAGhC,CAAC;AAIF,eAAO,MAAM,qBAAqB,0BAAoB,CAAC;AACvD,eAAO,MAAM,iBAAiB,0BAAgB,CAAC;AAC/C,eAAO,MAAM,iBAAiB,0BAAgB,CAAC;AAC/C,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,0BAAe,CAAC;AAC7C,eAAO,MAAM,kBAAkB,GAAI,OAAM,cAAmB,KAAG,MAA8B,CAAC;AAE9F,+EAA+E;AAC/E,eAAO,MAAM,UAAU,GAAI,OAAM,cAAmB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CACnF,CAAC;AAEvB,6EAA6E;AAC7E,eAAO,MAAM,YAAY,GAAI,OAAM,gBAAqB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CACrE,CAAC"}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AAuBjG,eAAO,MAAM,SAAS,EAAE,aAyXvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@intentius/chant": "^0.
|
|
79
|
+
"@intentius/chant": "^0.39.0",
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP read transport (#1209) — the applier's own REST client, pointed at the
|
|
3
|
+
* read side.
|
|
4
|
+
*
|
|
5
|
+
* GCP applies cluster-free over direct REST (#706) and, until this, observed
|
|
6
|
+
* through a Config Connector cluster: `kubectl get <cnrm-gvk> -o json` per
|
|
7
|
+
* entity, on both the thin and the deep path. That split is what #1085's
|
|
8
|
+
* principle forbids — a lexicon should be observed on the transport it is
|
|
9
|
+
* applied with, or the two can disagree about what a resource even is — and it
|
|
10
|
+
* is what made GCP the heaviest of the three O slots.
|
|
11
|
+
*
|
|
12
|
+
* ## The URL comes from the applier, not from here
|
|
13
|
+
*
|
|
14
|
+
* Every GET is built by the same `ResourceMapper.plan()` the applier uses, and
|
|
15
|
+
* this module reads `plan().getUrl` rather than composing its own URL. That is
|
|
16
|
+
* deliberate: a reader that builds its own paths is a second, silently
|
|
17
|
+
* divergent opinion about where a resource lives, and the first symptom is a
|
|
18
|
+
* read that reports absent for something the applier just wrote. Reusing the
|
|
19
|
+
* mapper makes the two structurally incapable of disagreeing.
|
|
20
|
+
*
|
|
21
|
+
* ## What that bounds
|
|
22
|
+
*
|
|
23
|
+
* Coverage is exactly the applier's dispatch table (`MAPPERS`) — StorageBucket,
|
|
24
|
+
* PubSubTopic, PubSubSubscription, SecretManagerSecret, IAMServiceAccount,
|
|
25
|
+
* RunService. The kubectl path could fetch any CNRM kind the cluster knew
|
|
26
|
+
* about, so this is narrower on paper. It is not narrower in practice for
|
|
27
|
+
* anything chant can act on: a kind with no mapper cannot be applied either, so
|
|
28
|
+
* observing it produced a live tree nothing could ever reconcile against.
|
|
29
|
+
*
|
|
30
|
+
* A kind outside the table reports NOT-OBSERVED with `unsupported-kind` rather
|
|
31
|
+
* than being dropped, so the estate says "chant did not look at this" instead of
|
|
32
|
+
* quietly implying it is not there. Widening the table is #1209's stated
|
|
33
|
+
* non-goal and belongs with the applier.
|
|
34
|
+
*/
|
|
35
|
+
import { MAPPERS, type GcpResource, type ResourceMapper } from "../op/activities/gcp-apply";
|
|
36
|
+
|
|
37
|
+
/** Where to read, and as whom. */
|
|
38
|
+
export interface GcpReadClientOptions {
|
|
39
|
+
/** Endpoint override for every kind (floci-gcp `http://localhost:4588`). Omit for real GCP. */
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
/** GCP project the resources live in. */
|
|
42
|
+
project: string;
|
|
43
|
+
/** Injectable HTTP, so tests never touch the network. */
|
|
44
|
+
http?: GcpReadHttp;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. */
|
|
48
|
+
export type GcpReadHttp = (method: string, url: string) => Promise<{ status: number; text: string }>;
|
|
49
|
+
|
|
50
|
+
const defaultHttp: GcpReadHttp = async (method, url) => {
|
|
51
|
+
const res = await fetch(url, { method });
|
|
52
|
+
return { status: res.status, text: await res.text() };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A read that failed for a reason the caller must classify — carries the HTTP
|
|
57
|
+
* status so a 404 (absent) is told apart from a 401/403 (no credentials) and a
|
|
58
|
+
* 5xx/network error (could not look). Mirrors `AzureReadError`.
|
|
59
|
+
*/
|
|
60
|
+
export class GcpReadError extends Error {
|
|
61
|
+
constructor(
|
|
62
|
+
message: string,
|
|
63
|
+
readonly status?: number,
|
|
64
|
+
) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "GcpReadError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A 404 — the resource is genuinely absent, which is an answer, not a failure. */
|
|
71
|
+
export function isNotFound(err: unknown): boolean {
|
|
72
|
+
return err instanceof GcpReadError && err.status === 404;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The mapper for a CNRM kind, or undefined when chant cannot act on it. */
|
|
76
|
+
export function mapperForKind(kind: string): ResourceMapper | undefined {
|
|
77
|
+
return MAPPERS[kind];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* GET one resource on the applier's transport.
|
|
82
|
+
*
|
|
83
|
+
* `declared` is the entity's own declared spec, which is what `plan()` needs to
|
|
84
|
+
* build a URL — the same input the applier hands it. Returns the parsed body,
|
|
85
|
+
* or throws {@link GcpReadError} carrying the status.
|
|
86
|
+
*/
|
|
87
|
+
export async function getResource(
|
|
88
|
+
client: GcpReadClientOptions,
|
|
89
|
+
kind: string,
|
|
90
|
+
name: string,
|
|
91
|
+
declared?: Record<string, unknown>,
|
|
92
|
+
): Promise<Record<string, unknown>> {
|
|
93
|
+
const mapper = mapperForKind(kind);
|
|
94
|
+
if (!mapper) throw new GcpReadError(`no REST mapper for kind ${kind}`);
|
|
95
|
+
|
|
96
|
+
const base = (client.endpoint ?? mapper.defaultHost).replace(/\/$/, "");
|
|
97
|
+
const resource: GcpResource = {
|
|
98
|
+
kind,
|
|
99
|
+
metadata: { name },
|
|
100
|
+
spec: (declared?.spec as Record<string, unknown> | undefined) ?? declared ?? {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let getUrl: string;
|
|
104
|
+
try {
|
|
105
|
+
getUrl = mapper.plan(resource, { base, project: client.project }).getUrl;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// A mapper that cannot plan from the declared spec cannot produce a URL, so
|
|
108
|
+
// there is nothing to read — a hole, never an absence.
|
|
109
|
+
throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const http = client.http ?? defaultHttp;
|
|
113
|
+
let res: { status: number; text: string };
|
|
114
|
+
try {
|
|
115
|
+
res = await http("GET", getUrl);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new GcpReadError(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (res.status === 404) throw new GcpReadError(`${kind}/${name} not found`, 404);
|
|
121
|
+
if (res.status < 200 || res.status >= 300) {
|
|
122
|
+
throw new GcpReadError(`GET ${kind}/${name} failed with ${res.status}: ${res.text.slice(0, 200)}`, res.status);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(res.text) as Record<string, unknown>;
|
|
126
|
+
} catch {
|
|
127
|
+
throw new GcpReadError(`GET ${kind}/${name} returned a non-JSON body`, res.status);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -42,7 +42,7 @@ export interface CloudFunctionWithTriggerProps {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export const CloudFunctionWithTrigger = Composite
|
|
45
|
+
export const CloudFunctionWithTrigger = Composite((props: CloudFunctionWithTriggerProps) => {
|
|
46
46
|
const {
|
|
47
47
|
name,
|
|
48
48
|
runtime,
|
|
@@ -56,7 +56,7 @@ export interface CloudRunServiceProps {
|
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
|
-
export const CloudRunServiceComposite = Composite
|
|
59
|
+
export const CloudRunServiceComposite = Composite((props: CloudRunServiceProps) => {
|
|
60
60
|
const {
|
|
61
61
|
name,
|
|
62
62
|
image,
|
|
@@ -52,7 +52,7 @@ export interface CloudSqlInstanceProps {
|
|
|
52
52
|
* });
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
|
-
export const CloudSqlInstance = Composite
|
|
55
|
+
export const CloudSqlInstance = Composite((props: CloudSqlInstanceProps) => {
|
|
56
56
|
const {
|
|
57
57
|
name,
|
|
58
58
|
databaseVersion = "POSTGRES_15",
|
|
@@ -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
|
+
}
|