@intentius/chant-lexicon-gcp 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.
Files changed (103) hide show
  1. package/dist/actions/index.js +45 -0
  2. package/dist/api/read-client.js +105 -0
  3. package/dist/codegen/docs-cli.js +6 -0
  4. package/dist/codegen/docs.js +115 -0
  5. package/dist/codegen/generate-cli.js +17 -0
  6. package/dist/codegen/generate.js +214 -0
  7. package/dist/codegen/naming.js +164 -0
  8. package/dist/codegen/package.js +43 -0
  9. package/dist/composites/catalog.js +1245 -0
  10. package/dist/composites/cloud-function.js +65 -0
  11. package/dist/composites/cloud-run-service.js +71 -0
  12. package/dist/composites/cloud-sql-instance.js +85 -0
  13. package/dist/composites/gcs-bucket.js +64 -0
  14. package/dist/composites/gke-cluster.js +113 -0
  15. package/dist/composites/gke-crdb-region.js +137 -0
  16. package/dist/composites/gpu-node-pool.js +74 -0
  17. package/dist/composites/index.js +15 -0
  18. package/dist/composites/landing-zone.js +89 -0
  19. package/dist/composites/managed-certificate.js +46 -0
  20. package/dist/composites/memorystore-redis.js +52 -0
  21. package/dist/composites/multi-region-vpc.js +108 -0
  22. package/dist/composites/private-service.js +50 -0
  23. package/dist/composites/pubsub-pipeline.js +63 -0
  24. package/dist/composites/secure-project.js +85 -0
  25. package/dist/composites/vpc-network.js +113 -0
  26. package/dist/coverage.js +28 -0
  27. package/dist/deep-observe-hooks.js +152 -0
  28. package/dist/deep-observe.js +273 -0
  29. package/dist/default-labels.js +67 -0
  30. package/dist/describe-resources.js +264 -0
  31. package/dist/detect.js +16 -0
  32. package/dist/export-resources.js +59 -0
  33. package/dist/generated/index.js +1804 -0
  34. package/dist/generated/runtime.js +4 -0
  35. package/dist/governance.js +98 -0
  36. package/dist/import/generator.js +68 -0
  37. package/dist/import/live-export.js +73 -0
  38. package/dist/import/parser.js +61 -0
  39. package/dist/index.js +36 -0
  40. package/dist/integrity.json +3 -3
  41. package/dist/lint/audit-catalog.js +38 -0
  42. package/dist/lint/audit-lineage.js +63 -0
  43. package/dist/lint/post-synth/gcp-helpers.js +91 -0
  44. package/dist/lint/post-synth/index.js +54 -0
  45. package/dist/lint/post-synth/schema-registry.d.ts.map +1 -1
  46. package/dist/lint/post-synth/schema-registry.js +69 -0
  47. package/dist/lint/post-synth/wgc101.js +47 -0
  48. package/dist/lint/post-synth/wgc102.js +29 -0
  49. package/dist/lint/post-synth/wgc103.js +37 -0
  50. package/dist/lint/post-synth/wgc104.js +34 -0
  51. package/dist/lint/post-synth/wgc105.js +39 -0
  52. package/dist/lint/post-synth/wgc106.js +30 -0
  53. package/dist/lint/post-synth/wgc107.js +33 -0
  54. package/dist/lint/post-synth/wgc108.js +34 -0
  55. package/dist/lint/post-synth/wgc109.js +33 -0
  56. package/dist/lint/post-synth/wgc110.js +32 -0
  57. package/dist/lint/post-synth/wgc111.js +47 -0
  58. package/dist/lint/post-synth/wgc112.js +46 -0
  59. package/dist/lint/post-synth/wgc113.js +35 -0
  60. package/dist/lint/post-synth/wgc201.js +30 -0
  61. package/dist/lint/post-synth/wgc202.js +33 -0
  62. package/dist/lint/post-synth/wgc203.js +35 -0
  63. package/dist/lint/post-synth/wgc204.js +33 -0
  64. package/dist/lint/post-synth/wgc301.js +28 -0
  65. package/dist/lint/post-synth/wgc302.js +28 -0
  66. package/dist/lint/post-synth/wgc303.js +29 -0
  67. package/dist/lint/post-synth/wgc401.js +50 -0
  68. package/dist/lint/post-synth/wgc402.js +46 -0
  69. package/dist/lint/post-synth/wgc403.js +74 -0
  70. package/dist/lint/post-synth/wgc501.js +63 -0
  71. package/dist/lint/post-synth/wgc502.js +37 -0
  72. package/dist/lint/post-synth/wgc503.js +49 -0
  73. package/dist/lint/rules/hardcoded-project.js +51 -0
  74. package/dist/lint/rules/hardcoded-region.js +51 -0
  75. package/dist/lint/rules/index.js +3 -0
  76. package/dist/lint/rules/public-iam.js +37 -0
  77. package/dist/lsp/completions.d.ts.map +1 -1
  78. package/dist/lsp/completions.js +15 -0
  79. package/dist/lsp/hover.d.ts.map +1 -1
  80. package/dist/lsp/hover.js +46 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/op/activities/floci-gcp.js +49 -0
  83. package/dist/op/activities/gcp-apply.js +618 -0
  84. package/dist/op/activities/index.js +10 -0
  85. package/dist/op/builders.js +50 -0
  86. package/dist/ownership.js +38 -0
  87. package/dist/package-cli.js +20 -0
  88. package/dist/plugin.js +371 -0
  89. package/dist/pseudo.js +15 -0
  90. package/dist/rules/schema-registry.ts +2 -1
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/serializer.js +233 -0
  93. package/dist/spec/fetch.js +107 -0
  94. package/dist/spec/parse.js +323 -0
  95. package/dist/validate-cli.js +11 -0
  96. package/dist/validate.js +24 -0
  97. package/dist/variables.js +15 -0
  98. package/package.json +8 -8
  99. package/src/codegen/package.ts +2 -2
  100. package/src/lint/post-synth/schema-registry.ts +2 -1
  101. package/src/lsp/completions.ts +2 -1
  102. package/src/lsp/hover.ts +2 -1
  103. package/src/serializer.ts +2 -1
@@ -0,0 +1,152 @@
1
+ import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
2
+ import { GCP_RESOURCE_OWNERSHIP_KEYS } from "./ownership.js";
3
+ import { resolveGVK } from "./serializer.js";
4
+ /**
5
+ * Fields GCP populates on essentially anything, wherever they appear.
6
+ *
7
+ * Pruned on BOTH sides and regardless of whether source declared them: these
8
+ * are server-assigned identity and bookkeeping, and a user who writes one is
9
+ * writing something the API will overwrite anyway.
10
+ */
11
+ export const GCP_READ_ONLY_NAMES = new Set([
12
+ "etag",
13
+ "selfLink",
14
+ "id",
15
+ "projectId",
16
+ "projectNumber",
17
+ "timeCreated",
18
+ "updated",
19
+ "createTime",
20
+ "updateTime",
21
+ "generation",
22
+ "metageneration",
23
+ "uid",
24
+ "uniqueId",
25
+ "email",
26
+ "state",
27
+ "reconciling",
28
+ "observedGeneration",
29
+ // Cloud Run's server-computed service surface (v2 GetService).
30
+ "uri",
31
+ "urls",
32
+ "conditions",
33
+ "terminalCondition",
34
+ "latestReadyRevision",
35
+ "latestCreatedRevision",
36
+ ]);
37
+ /**
38
+ * Per-kind values GCP fills in when the request omits them, keyed by the
39
+ * index-erased pattern from the tree root (the declared-props shape
40
+ * `./deep-observe.ts` reshapes the live payload into — spec fields at the
41
+ * root, so `storageClass`, not `spec.storageClass`).
42
+ *
43
+ * Unlike {@link GCP_READ_ONLY_NAMES} these are only noise where **source never
44
+ * declared the property** — a default somebody explicitly wrote is a fact worth
45
+ * diffing, and pruning it would hide a real change away from it. The
46
+ * `counterpart === "absent"` gate below is what enforces that, and it is the
47
+ * same rule the AWS and Azure hooks apply.
48
+ */
49
+ export const GCP_SERVICE_DEFAULTS = {
50
+ StorageBucket: {
51
+ storageClass: "STANDARD",
52
+ locationType: "multi-region",
53
+ publicAccessPrevention: "inherited",
54
+ uniformBucketLevelAccess: false,
55
+ "softDeletePolicy.retentionDurationSeconds": "604800",
56
+ },
57
+ PubSubTopic: {
58
+ "messageStoragePolicy.enforceInTransit": false,
59
+ },
60
+ PubSubSubscription: {
61
+ ackDeadlineSeconds: 10,
62
+ "expirationPolicy.ttl": "2678400s",
63
+ enableMessageOrdering: false,
64
+ enableExactlyOnceDelivery: false,
65
+ retainAckedMessages: false,
66
+ detached: false,
67
+ "retryPolicy.minimumBackoff": "10s",
68
+ },
69
+ RunService: {
70
+ ingress: "INGRESS_TRAFFIC_ALL",
71
+ launchStage: "GA",
72
+ },
73
+ IAMServiceAccount: {
74
+ disabled: false,
75
+ },
76
+ };
77
+ /**
78
+ * chant's own ownership marker, in both vocabularies it is written in: the
79
+ * serializer stamps k8s-style label keys on the CNRM object
80
+ * (`app.kubernetes.io/managed-by`), while `gcpApply` stamps the GCP-legal
81
+ * equivalents on the live resource (`managed-by` — GCP label keys allow no `/`
82
+ * or `.`). See `./ownership.ts` for why the two surfaces differ. A declared
83
+ * tree carries the first family, a live payload can carry either (the applier
84
+ * forwards the manifest's labels where a kind accepts labels at all), and
85
+ * neither is drift: it is chant showing its own signature to itself — the same
86
+ * correction #1301 made for AWS.
87
+ */
88
+ const CHANT_OWNERSHIP_LABEL_PATTERNS = new Set([LABEL_OWNERSHIP_KEYS, GCP_RESOURCE_OWNERSHIP_KEYS].flatMap((keys) => [
89
+ `metadata.labels.${keys.managedBy}`,
90
+ `metadata.labels.${keys.stack}`,
91
+ `metadata.labels.${keys.env}`,
92
+ ]));
93
+ /** Declared-side CNRM machinery a REST payload never echoes (see module doc). */
94
+ const CNRM_ENVELOPE_PATTERNS = new Set(["projectRef", "resourceID"]);
95
+ const CNRM_ANNOTATION_PREFIX = "metadata.annotations.cnrm.cloud.google.com/";
96
+ /** Last dotted segment of a path pattern — `spec.foo.etag` -> `etag`. */
97
+ function lastSegment(pattern) {
98
+ const i = pattern.lastIndexOf(".");
99
+ return i === -1 ? pattern : pattern.slice(i + 1);
100
+ }
101
+ /** A `{}` with nothing pruned out of it, as GCP returns for an unset message. */
102
+ function isEmptyObject(value) {
103
+ return (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0);
104
+ }
105
+ /**
106
+ * GCP's static deep-observation noise rules, applied by core to the declared
107
+ * tree and the live tree alike so the two are compared in the same shape.
108
+ */
109
+ export const gcpDeepNormalizationHooks = {
110
+ prune(node) {
111
+ // Server-assigned, wherever it appears, on either side.
112
+ if (GCP_READ_ONLY_NAMES.has(lastSegment(node.pattern)))
113
+ return true;
114
+ // chant's own ownership marker is not drift (see the set's doc).
115
+ if (CHANT_OWNERSHIP_LABEL_PATTERNS.has(node.pattern))
116
+ return true;
117
+ // CNRM machinery on the declared side: instructions to the controller, not
118
+ // resource state, and no REST payload ever carries them.
119
+ if (CNRM_ENVELOPE_PATTERNS.has(node.pattern))
120
+ return true;
121
+ if (node.pattern.startsWith(CNRM_ANNOTATION_PREFIX))
122
+ return true;
123
+ // Everything below subtracts provider-populated values from the live tree,
124
+ // and only where source never declared the property. `counterpart` is a
125
+ // tri-state; only `absent` licenses it.
126
+ if (node.side !== "live" || node.counterpart !== "absent")
127
+ return false;
128
+ // An undeclared `null` or `{}` is GCP's spelling of "unset" — a field the
129
+ // API includes in the payload without there being any configuration in it
130
+ // (a service account's `description: null`, Cloud Run's
131
+ // `binaryAuthorization: {}`).
132
+ if (node.value === null || isEmptyObject(node.value))
133
+ return true;
134
+ const kindDefaults = GCP_SERVICE_DEFAULTS[shortKind(node.entityType)];
135
+ if (!kindDefaults)
136
+ return false;
137
+ return Object.prototype.hasOwnProperty.call(kindDefaults, node.pattern) && kindDefaults[node.pattern] === node.value;
138
+ },
139
+ };
140
+ /**
141
+ * `GCP::Pubsub::Topic` -> `PubSubTopic`, the key `GCP_SERVICE_DEFAULTS` uses —
142
+ * the CNRM kind, resolved through the serializer's GVK map because the naive
143
+ * `${service}${shortKind}` concatenation miscases 4 of the 6 appliable kinds
144
+ * (`PubsubTopic`, `IamServiceAccount`, …) and silently matches no defaults.
145
+ */
146
+ function shortKind(entityType) {
147
+ const resolved = resolveGVK(entityType);
148
+ if (resolved)
149
+ return resolved.kind;
150
+ const parts = entityType.split("::");
151
+ return parts.length === 3 ? `${parts[1]}${parts[2]}` : entityType;
152
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * GCP deep observation (#1087, re-landed on direct REST by #1209/#1210) — the
3
+ * GCP row of the deep-observe contract (#1014).
4
+ *
5
+ * ## The reader is the applier's transport, pointed at reads
6
+ *
7
+ * chant applies GCP cluster-free over direct REST (#706), and since #1209 it
8
+ * observes the same way: every GET here is built by the same
9
+ * `ResourceMapper.plan().getUrl` the applier writes through (see
10
+ * ./api/read-client.ts), so reader and applier cannot disagree about where a
11
+ * resource lives. `GCP_ENDPOINT_URL` redirects the whole reader at a local
12
+ * floci-gcp; unset means real GCP, exactly as it does for the applier.
13
+ * Coverage is the applier's own dispatch table — a kind with no mapper cannot
14
+ * be applied either, and reports NOT-OBSERVED with `unsupported-kind` (#1089)
15
+ * rather than being dropped.
16
+ *
17
+ * ## Two shapes, one diff
18
+ *
19
+ * The declared side is a CNRM spec as a declarable authors it (`{ metadata:
20
+ * { name }, location, storageClass }`); the REST APIs return their own shapes
21
+ * (`name` is usually the full resource path, a bucket's `iamConfiguration`
22
+ * nests what CNRM flattens, a subscription's `topic` is a path where CNRM
23
+ * writes `topicRef`). Diffed raw, every such field drifts twice: once as
24
+ * `declared -> <absent>` and once as `<undeclared> -> live`.
25
+ * {@link restToCnrmShape} plus the per-kind {@link CNRM_REFINERS} put the live
26
+ * payload into the declared vocabulary before the diff runs — the same move
27
+ * the AWS row makes with its per-type `toModel` (#1207/#1269), and it lives
28
+ * next to the read for the same reason: translation is the price of the
29
+ * richer read, and it does not belong inside the diff.
30
+ *
31
+ * ## Noise is a static table, not an ownership walk
32
+ *
33
+ * The kubectl-era reader pruned by CNRM `metadata.managedFields`. A REST
34
+ * payload never says who wrote a field, so the noise rules are static
35
+ * (./deep-observe-hooks.ts): server-assigned names, chant's own ownership
36
+ * labels, CNRM-only declared fields, and per-kind provider defaults gated on
37
+ * "source never declared it". Core applies the same hooks to both trees.
38
+ *
39
+ * Since #2160 part of that table has a substrate-independent answer behind it:
40
+ * core derives a claimed-field set from the declaration's own props, and a
41
+ * live value on a path outside it is reported as unclaimed rather than as
42
+ * drift. The table is not obsolete — `prune` is still the right tool for a
43
+ * field GCP populates on every object of a kind, and for putting the two trees
44
+ * in the same vocabulary — but the entries that exist only to say "nobody
45
+ * declared this" now have a general mechanism underneath them.
46
+ *
47
+ * ## The build-path boundary
48
+ *
49
+ * `gcpPlugin.ts` reaches this file only via `await import("./deep-observe.js")`
50
+ * inside `observeResourcesDeep` — never statically — so `chant build` never
51
+ * resolves the live transport just to synthesize a template.
52
+ * `deepNormalizationHooks` is plain data (./deep-observe-hooks.ts) and is
53
+ * imported statically from `plugin.ts`, because core normalizes the
54
+ * *declared* tree with it whether or not a live read ever happens.
55
+ */
56
+ import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
57
+ import { hasOwnershipMarker } from "@intentius/chant/ownership";
58
+ import { deriveGVK, manifestProjectAnnotations, resolveReadProject } from "./describe-resources.js";
59
+ import { getResource, mapperForKind, GcpReadError, isNotFound } from "./api/read-client.js";
60
+ import { gcpDeepNormalizationHooks } from "./deep-observe-hooks.js";
61
+ /** The labels the applier stamps — see describe-resources.ts. */
62
+ const GCP_OWNERSHIP_LABEL_KEYS = {
63
+ managedBy: "managed-by",
64
+ stack: "chant-stack",
65
+ env: "chant-env",
66
+ };
67
+ // Re-exported so a dynamic importer of this module (plugin.ts's
68
+ // `observeResourcesDeep`, a test) can get the reader and its hooks from one
69
+ // place. `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
70
+ // separately, directly from `./deep-observe-hooks` — see the module doc.
71
+ export { gcpDeepNormalizationHooks };
72
+ /**
73
+ * Reshape a GCP REST body into the shape the declared props are written in:
74
+ * identity and labels under `metadata`, everything else at the root — a CNRM
75
+ * spec as a chant declarable authors it (`new StorageBucket({ metadata,
76
+ * location, storageClass })`). Kind-specific vocabulary differences on top of
77
+ * it are {@link CNRM_REFINERS}' job. Verified against floci-gcp: diffed raw, a
78
+ * bucket that matched its declaration exactly reported every field as drift
79
+ * twice, once per shape.
80
+ */
81
+ export function restToCnrmShape(body) {
82
+ const metadata = {};
83
+ const rest = {};
84
+ for (const [key, value] of Object.entries(body)) {
85
+ if (key === "name" || key === "labels" || key === "annotations")
86
+ metadata[key] = value;
87
+ else
88
+ rest[key] = value;
89
+ }
90
+ return {
91
+ ...(Object.keys(metadata).length ? { metadata } : {}),
92
+ ...rest,
93
+ };
94
+ }
95
+ /** The live payload minus the fields that live outside `properties` on
96
+ * {@link DeepResourceObservation}. A REST body has no `apiVersion`, but `kind`
97
+ * shows up on some (GCS returns `storage#bucket`), so both are dropped. */
98
+ function propertiesTreeOf(obj) {
99
+ const { apiVersion: _apiVersion, kind: _kind, ...rest } = obj;
100
+ return rest;
101
+ }
102
+ /**
103
+ * Per-kind REST -> CNRM vocabulary mapping, applied after the generic
104
+ * {@link restToCnrmShape}. Each entry translates exactly the fields where the
105
+ * REST API and the CNRM schema disagree about names or nesting — kept next to
106
+ * the kind, like the applier's forward mapping in gcp-apply.ts, so adding a
107
+ * field means touching one kind in one file per direction.
108
+ */
109
+ const CNRM_REFINERS = {
110
+ StorageBucket(tree) {
111
+ // CNRM flattens GCS's `iamConfiguration` envelope.
112
+ const iam = tree.iamConfiguration;
113
+ if (iam) {
114
+ if (iam.uniformBucketLevelAccess?.enabled !== undefined) {
115
+ tree.uniformBucketLevelAccess = iam.uniformBucketLevelAccess.enabled;
116
+ }
117
+ if (iam.publicAccessPrevention !== undefined)
118
+ tree.publicAccessPrevention = iam.publicAccessPrevention;
119
+ delete tree.iamConfiguration;
120
+ }
121
+ // `lifecycle.rule` -> `lifecycleRule`, the inverse of bucketInsertBody.
122
+ const lifecycle = tree.lifecycle;
123
+ if (lifecycle) {
124
+ if (lifecycle.rule !== undefined)
125
+ tree.lifecycleRule = lifecycle.rule;
126
+ delete tree.lifecycle;
127
+ }
128
+ },
129
+ PubSubSubscription(tree, ctx) {
130
+ const topic = tree.topic;
131
+ if (typeof topic !== "string")
132
+ return;
133
+ // The declared side writes a `topicRef`; a same-project path maps to the
134
+ // `name` form the manifest overwhelmingly uses, anything else to `external`.
135
+ const prefix = `projects/${ctx.project}/topics/`;
136
+ tree.topicRef = topic.startsWith(prefix) ? { name: topic.slice(prefix.length) } : { external: topic };
137
+ delete tree.topic;
138
+ },
139
+ SecretManagerSecret(tree) {
140
+ // CNRM spells automatic replication as a boolean; the REST API as `{}`.
141
+ const replication = tree.replication;
142
+ if (replication && typeof replication.automatic === "object" && replication.automatic !== null) {
143
+ replication.automatic = true;
144
+ }
145
+ },
146
+ IAMServiceAccount(tree, ctx) {
147
+ // The REST identity is the derived email; the declared name is the account
148
+ // id it was derived from (see gcpServiceAccountMapper).
149
+ const name = tree.metadata?.name;
150
+ if (typeof name === "string" && name.endsWith(`@${ctx.project}.iam.gserviceaccount.com`)) {
151
+ tree.metadata.name = name.slice(0, name.indexOf("@"));
152
+ }
153
+ },
154
+ RunService(tree, ctx) {
155
+ // The v2 payload carries no `location` field — it lives in the resource
156
+ // name. The declared side writes it as `location`.
157
+ const m = ctx.restName ? /\/locations\/([^/]+)\//.exec(ctx.restName) : null;
158
+ if (m)
159
+ tree.location = m[1];
160
+ },
161
+ };
162
+ /**
163
+ * A REST payload as a CNRM-shaped property tree, in the declared side's
164
+ * vocabulary. The generic reshape runs first, then the full resource path is
165
+ * shortened to the declared `metadata.name` (`projects/p/topics/t` -> `t`),
166
+ * then the kind's own refiner translates what the two schemas name
167
+ * differently.
168
+ */
169
+ export function toCnrmTree(kind, body, ctx) {
170
+ const tree = restToCnrmShape(propertiesTreeOf(body));
171
+ const restName = typeof body.name === "string" ? body.name : undefined;
172
+ const name = tree.metadata?.name;
173
+ if (typeof name === "string" && name.includes("/")) {
174
+ tree.metadata.name = name.slice(name.lastIndexOf("/") + 1);
175
+ }
176
+ CNRM_REFINERS[kind]?.(tree, { ...ctx, restName });
177
+ return tree;
178
+ }
179
+ /**
180
+ * Read the live property tree for each declared entity over the applier's
181
+ * REST transport, normalized into the declared CNRM shape (see the module
182
+ * doc). A connect failure becomes NOT-OBSERVED for every declared entity
183
+ * rather than an empty result, and a 404 is a real absence the thin read
184
+ * already reports.
185
+ */
186
+ export async function observeResourcesDeepGcp(options) {
187
+ const resources = {};
188
+ const unobserved = {};
189
+ const endpoint = process.env.GCP_ENDPOINT_URL;
190
+ const manifestProjects = manifestProjectAnnotations(options.buildOutput);
191
+ const reads = [...options.entities].map(async ([entityName, { entityType, props }]) => {
192
+ const gvk = deriveGVK(entityType);
193
+ if (!gvk || !mapperForKind(gvk.kind)) {
194
+ unobserved[entityName] = {
195
+ type: entityType,
196
+ reason: "unsupported-kind",
197
+ detail: gvk
198
+ ? `no REST mapper for ${gvk.kind} — chant cannot apply this kind either`
199
+ : `cannot derive a GCP kind from ${entityType}`,
200
+ };
201
+ return;
202
+ }
203
+ const metadata = props.metadata;
204
+ const name = metadata?.name;
205
+ if (!name) {
206
+ unobserved[entityName] = {
207
+ type: entityType,
208
+ reason: "read-failed",
209
+ detail: "declared entity has no metadata.name to query by",
210
+ };
211
+ return;
212
+ }
213
+ let client;
214
+ try {
215
+ client = {
216
+ project: resolveReadProject(gvk.kind, name, metadata, manifestProjects),
217
+ ...(endpoint ? { endpoint } : {}),
218
+ };
219
+ }
220
+ catch (err) {
221
+ unobserved[entityName] = {
222
+ type: entityType,
223
+ reason: "no-binding",
224
+ detail: err instanceof Error ? err.message : String(err),
225
+ };
226
+ return;
227
+ }
228
+ try {
229
+ const obj = await getResource(client, gvk.kind, name, props);
230
+ const labels = obj.labels;
231
+ // owned filter: withhold what does not carry chant's marker. Withheld is
232
+ // not absent (#1089). Where the payload has no labels at all there is
233
+ // nothing to filter on, so the resource passes through — the same
234
+ // detect-only degradation the thin path takes.
235
+ if (options.owned && labels != null && !hasOwnershipMarker(labels, GCP_OWNERSHIP_LABEL_KEYS)) {
236
+ unobserved[entityName] = {
237
+ type: entityType,
238
+ reason: "filtered",
239
+ detail: "live resource carries no chant ownership marker and --owned was requested",
240
+ };
241
+ return;
242
+ }
243
+ resources[entityName] = {
244
+ type: entityType,
245
+ physicalId: obj.id ?? obj.selfLink ?? obj.name,
246
+ properties: normalizeDeepProperties(toCnrmTree(gvk.kind, obj, { project: client.project }), {
247
+ entityType,
248
+ side: "live",
249
+ // The static table is the whole prune here — there is no per-resource
250
+ // ownership pass, because a REST payload carries no field ownership
251
+ // to drive one (see ./deep-observe-hooks.ts). What the payload cannot
252
+ // say, the declaration does: core classifies against the claimed-field
253
+ // set above this reader (#2160).
254
+ hooks: gcpDeepNormalizationHooks,
255
+ }),
256
+ };
257
+ }
258
+ catch (err) {
259
+ // A 404 is a real absence, same as the thin read — recorded there, not
260
+ // restated here. Anything else proves nothing and is a hole (#1089).
261
+ if (isNotFound(err))
262
+ return;
263
+ const status = err instanceof GcpReadError ? err.status : undefined;
264
+ unobserved[entityName] = {
265
+ type: entityType,
266
+ reason: status === 401 || status === 403 ? "no-credentials" : "read-failed",
267
+ detail: err instanceof Error ? err.message : String(err),
268
+ };
269
+ }
270
+ });
271
+ await Promise.all(reads);
272
+ return deepObservation(resources, unobserved);
273
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Default Labels & Annotations — declares project-wide labels/annotations
3
+ * for all GCP Config Connector 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
+ export const DEFAULT_LABELS_MARKER = Symbol.for("chant.gcp.defaultLabels");
12
+ export const DEFAULT_ANNOTATIONS_MARKER = Symbol.for("chant.gcp.defaultAnnotations");
13
+ export function isDefaultLabels(value) {
14
+ return (typeof value === "object" &&
15
+ value !== null &&
16
+ DEFAULT_LABELS_MARKER in value &&
17
+ value[DEFAULT_LABELS_MARKER] === true);
18
+ }
19
+ export function isDefaultAnnotations(value) {
20
+ return (typeof value === "object" &&
21
+ value !== null &&
22
+ DEFAULT_ANNOTATIONS_MARKER in value &&
23
+ value[DEFAULT_ANNOTATIONS_MARKER] === true);
24
+ }
25
+ /**
26
+ * Declare project-wide default labels for all GCP Config Connector resources.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { defaultLabels } from "@intentius/chant-lexicon-gcp";
31
+ *
32
+ * export const labels = defaultLabels({
33
+ * "app.kubernetes.io/managed-by": "chant",
34
+ * "env": "production",
35
+ * });
36
+ * ```
37
+ */
38
+ export function defaultLabels(labels) {
39
+ return {
40
+ [DEFAULT_LABELS_MARKER]: true,
41
+ [DECLARABLE_MARKER]: true,
42
+ lexicon: "gcp",
43
+ entityType: "chant:gcp:defaultLabels",
44
+ labels,
45
+ };
46
+ }
47
+ /**
48
+ * Declare project-wide default annotations for all GCP Config Connector resources.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { defaultAnnotations, GCP } from "@intentius/chant-lexicon-gcp";
53
+ *
54
+ * export const annotations = defaultAnnotations({
55
+ * "cnrm.cloud.google.com/project-id": GCP.ProjectId,
56
+ * });
57
+ * ```
58
+ */
59
+ export function defaultAnnotations(annotations) {
60
+ return {
61
+ [DEFAULT_ANNOTATIONS_MARKER]: true,
62
+ [DECLARABLE_MARKER]: true,
63
+ lexicon: "gcp",
64
+ entityType: "chant:gcp:defaultAnnotations",
65
+ annotations,
66
+ };
67
+ }