@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,264 @@
1
+ /**
2
+ * GCP thin observation (#1209) — presence and scrubbed outputs, read over the
3
+ * applier's own REST transport.
4
+ *
5
+ * chant applies GCP cluster-free over direct REST (#706). Until this, it
6
+ * observed through a Config Connector cluster — `kubectl get <cnrm-gvk> -o
7
+ * json` per entity — which is the split #1085's principle forbids: a lexicon
8
+ * observed on a different transport than it is applied with can disagree with
9
+ * itself about what a resource even is, and needs a GKE cluster to answer a
10
+ * question about a bucket.
11
+ *
12
+ * Every GET here is built by the same `ResourceMapper` the applier uses (see
13
+ * ./api/read-client.ts), so reader and applier cannot diverge about where a
14
+ * resource lives.
15
+ *
16
+ * ## What changed in the answers, not just the transport
17
+ *
18
+ * **Status.** Config Connector encodes state as a `Ready` condition, which a
19
+ * GCP REST payload has no equivalent of — a bucket simply exists. So a
20
+ * successful GET is `PRESENT` unless the body carries a recognisable state of
21
+ * its own (Cloud Run's `status.conditions`, an explicit `state`). `PRESENT` is
22
+ * the same sentinel the Azure reader emits for the same reason.
23
+ *
24
+ * **Ownership.** CNRM carried chant's marker as a k8s label. The REST payloads
25
+ * carry `labels` only on kinds that have them (a bucket does; a Pub/Sub topic
26
+ * does not), so ownership is `unknown` where there is nothing to read — and
27
+ * `--owned` degrades to detect-only rather than withholding everything it
28
+ * cannot prove, the same posture the AWS thin path takes when
29
+ * `describe-stack-resources` returns no tags.
30
+ *
31
+ * **Coverage.** kubectl could fetch any CNRM kind the cluster knew about; REST
32
+ * reaches the kinds the applier has a mapper for. That is narrower on paper and
33
+ * not in practice: a kind with no mapper cannot be applied either, so observing
34
+ * it produced a live tree nothing could reconcile against. Anything outside the
35
+ * table reports NOT-OBSERVED with `unsupported-kind` (#1089) rather than being
36
+ * dropped.
37
+ */
38
+ import { observation } from "@intentius/chant/observation";
39
+ import { hasOwnershipMarker, classifyOwnership, readOwnership } from "@intentius/chant/ownership";
40
+ import { getResource, mapperForKind, GcpReadError, isNotFound } from "./api/read-client.js";
41
+ import { resolveGcpProject, parseManifest } from "./op/activities/gcp-apply.js";
42
+ import { resolveGVK } from "./serializer.js";
43
+ /**
44
+ * chant's ownership marker as GCP labels.
45
+ *
46
+ * The applier stamps `managed-by: chant` (gcp-apply.ts) because GCP label keys
47
+ * cannot hold the k8s `app.kubernetes.io/managed-by` slash/dot form that
48
+ * `LABEL_OWNERSHIP_KEYS` uses — so the reader has to look for what the applier
49
+ * actually wrote, not for the CNRM label the kubectl path read. The stack/env
50
+ * keys follow the same flattening.
51
+ */
52
+ const GCP_OWNERSHIP_LABEL_KEYS = {
53
+ managedBy: "managed-by",
54
+ stack: "chant-stack",
55
+ env: "chant-env",
56
+ };
57
+ /**
58
+ * The CNRM group/kind for an entity type, resolved through the serializer's
59
+ * own GVK map so the reader and the manifest agree on the kind's casing —
60
+ * `GCP::Pubsub::Topic` is `PubSubTopic` in CNRM and in the applier's mapper
61
+ * table, while a naive `${service}${shortKind}` concatenation would produce
62
+ * `PubsubTopic` and silently miss the mapper for 4 of the 6 appliable kinds.
63
+ */
64
+ export function deriveGVK(entityType) {
65
+ const resolved = resolveGVK(entityType);
66
+ if (!resolved)
67
+ return null;
68
+ return { group: resolved.apiVersion.split("/")[0], kind: resolved.kind };
69
+ }
70
+ /**
71
+ * The `cnrm.cloud.google.com/project-id` annotations in the built manifest,
72
+ * keyed `kind/name`.
73
+ *
74
+ * The declared entities the observe paths receive are discovery output, and a
75
+ * project-wide `defaultAnnotations({"cnrm.cloud.google.com/project-id": …})`
76
+ * is a separate declarable the serializer merges into each document at
77
+ * synthesis — the entity's own props never see it. The applier resolves its
78
+ * project from the manifest for exactly that reason, so the readers do too:
79
+ * without this, every entity relying on the default annotation reports
80
+ * `no-binding` unless the shell happens to export GOOGLE_CLOUD_PROJECT.
81
+ */
82
+ export function manifestProjectAnnotations(buildOutput) {
83
+ const out = new Map();
84
+ if (!buildOutput)
85
+ return out;
86
+ try {
87
+ for (const doc of parseManifest(buildOutput, "<build output>")) {
88
+ const project = doc.metadata?.annotations?.["cnrm.cloud.google.com/project-id"];
89
+ if (doc.kind && doc.metadata?.name && typeof project === "string") {
90
+ out.set(`${doc.kind}/${doc.metadata.name}`, project);
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ // An unparsable build output resolves nothing — per-entity annotations and
96
+ // GOOGLE_CLOUD_PROJECT still apply, and a truly unresolvable project is
97
+ // still reported as `no-binding` per entity.
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * The project a read should target: `GOOGLE_CLOUD_PROJECT` / the entity's own
103
+ * annotation (via `resolveGcpProject`, the applier's rule), then the built
104
+ * manifest's merged annotation. Throws the applier's own error when nothing
105
+ * resolves, so the `no-binding` detail names both knobs.
106
+ */
107
+ export function resolveReadProject(kind, name, metadata, manifestProjects) {
108
+ try {
109
+ return resolveGcpProject({ kind, metadata });
110
+ }
111
+ catch (err) {
112
+ const fromManifest = manifestProjects.get(`${kind}/${name}`);
113
+ if (fromManifest)
114
+ return fromManifest;
115
+ throw err;
116
+ }
117
+ }
118
+ function pruneUndefined(obj) {
119
+ const out = {};
120
+ for (const [k, v] of Object.entries(obj)) {
121
+ if (v !== undefined)
122
+ out[k] = v;
123
+ }
124
+ return out;
125
+ }
126
+ /**
127
+ * Status from a REST payload.
128
+ *
129
+ * Most GCP resources have no status at all — a bucket that answers a GET simply
130
+ * exists — so `PRESENT` is the honest answer and the common one. Where a kind
131
+ * does carry state (Cloud Run's `status.conditions`, a `state` enum), that is
132
+ * reported instead.
133
+ *
134
+ * `PRESENT` is deliberately the same sentinel the Azure reader emits for a
135
+ * resource with no `provisioningState`: one word, meaning "read it back, it is
136
+ * there, there is nothing richer to say".
137
+ */
138
+ export function statusFromRest(obj) {
139
+ if (typeof obj.state === "string" && obj.state.length > 0)
140
+ return obj.state;
141
+ const conditions = obj.status?.conditions ?? [];
142
+ const ready = conditions.find((c) => c.type === "Ready");
143
+ if (ready) {
144
+ if (ready.status === "True" || ready.state === "CONDITION_SUCCEEDED")
145
+ return "READY";
146
+ return ready.reason ?? "NOT_READY";
147
+ }
148
+ if (conditions.length > 0) {
149
+ return conditions.map((c) => `${c.type}=${c.status ?? c.state}`).join(",");
150
+ }
151
+ return "PRESENT";
152
+ }
153
+ export async function describeResources(options) {
154
+ const result = {};
155
+ const unobserved = {};
156
+ // The endpoint override is the emulator tell (floci-gcp :4588); unset means
157
+ // real GCP, exactly as it does for the applier.
158
+ const endpoint = process.env.GCP_ENDPOINT_URL;
159
+ // `--owned` needs labels to filter on, and only some kinds carry them. Rather
160
+ // than withhold everything it cannot prove, this warns once and degrades to
161
+ // detect-only — the same posture the AWS thin path takes when
162
+ // `describe-stack-resources` returns no tags.
163
+ let warnedOwnership = false;
164
+ const manifestProjects = manifestProjectAnnotations(options.buildOutput);
165
+ const reads = [...options.entities].map(async ([entityName, { entityType, props }]) => {
166
+ const gvk = deriveGVK(entityType);
167
+ if (!gvk) {
168
+ unobserved[entityName] = {
169
+ type: entityType,
170
+ reason: "unsupported-kind",
171
+ detail: `cannot derive a GCP kind from ${entityType}`,
172
+ };
173
+ return;
174
+ }
175
+ if (!mapperForKind(gvk.kind)) {
176
+ // Outside the applier's dispatch table: chant cannot write this kind, so
177
+ // it does not claim to have read it either (#1089).
178
+ unobserved[entityName] = {
179
+ type: entityType,
180
+ reason: "unsupported-kind",
181
+ detail: `no REST mapper for ${gvk.kind} — chant cannot apply this kind either`,
182
+ };
183
+ return;
184
+ }
185
+ const metadata = props.metadata;
186
+ const name = metadata?.name;
187
+ if (!name) {
188
+ unobserved[entityName] = {
189
+ type: entityType,
190
+ reason: "read-failed",
191
+ detail: "declared entity has no metadata.name to query by",
192
+ };
193
+ return;
194
+ }
195
+ let client;
196
+ try {
197
+ client = {
198
+ project: resolveReadProject(gvk.kind, name, metadata, manifestProjects),
199
+ ...(endpoint ? { endpoint } : {}),
200
+ };
201
+ }
202
+ catch (err) {
203
+ // No project resolvable — nothing was queried, so nothing is known.
204
+ unobserved[entityName] = {
205
+ type: entityType,
206
+ reason: "no-binding",
207
+ detail: err instanceof Error ? err.message : String(err),
208
+ };
209
+ return;
210
+ }
211
+ try {
212
+ const obj = (await getResource(client, gvk.kind, name, props));
213
+ if (options.owned) {
214
+ if (obj.labels == null) {
215
+ if (!warnedOwnership) {
216
+ // eslint-disable-next-line no-console
217
+ console.warn(`[gcp] ownership filter unavailable for ${gvk.kind} (the REST payload carries no labels) — returning it with an \`unknown\` verdict rather than withholding it`);
218
+ warnedOwnership = true;
219
+ }
220
+ }
221
+ else if (!hasOwnershipMarker(obj.labels, GCP_OWNERSHIP_LABEL_KEYS)) {
222
+ // Withheld is not absent (#1089) — the resource exists, it just isn't chant's.
223
+ unobserved[entityName] = {
224
+ type: entityType,
225
+ reason: "filtered",
226
+ detail: "live resource carries no chant ownership marker and --owned was requested",
227
+ };
228
+ return;
229
+ }
230
+ }
231
+ result[entityName] = {
232
+ type: entityType,
233
+ physicalId: obj.id ?? obj.selfLink ?? obj.name,
234
+ status: statusFromRest(obj),
235
+ lastUpdated: obj.updated ?? obj.timeCreated,
236
+ ownership: obj.labels == null ? "unknown" : classifyOwnership(obj.labels, GCP_OWNERSHIP_LABEL_KEYS),
237
+ // Marker identity (#1222): stack/env from the resource's own labels.
238
+ // Undefined when the payload carries no labels or no managed-by marker.
239
+ marker: readOwnership(obj.labels ?? undefined, GCP_OWNERSHIP_LABEL_KEYS),
240
+ attributes: pruneUndefined({
241
+ labels: obj.labels ?? undefined,
242
+ selfLink: obj.selfLink,
243
+ }),
244
+ };
245
+ }
246
+ catch (err) {
247
+ // A 404 is a real absence. Anything else — no credentials, an
248
+ // unreachable endpoint, a body that will not parse — proves nothing and
249
+ // is a hole rather than an absence (#1089).
250
+ if (isNotFound(err))
251
+ return;
252
+ const status = err instanceof GcpReadError ? err.status : undefined;
253
+ const noCreds = status === 401 || status === 403;
254
+ unobserved[entityName] = {
255
+ type: entityType,
256
+ reason: noCreds ? "no-credentials" : "read-failed",
257
+ detail: err instanceof Error ? err.message : String(err),
258
+ };
259
+ }
260
+ });
261
+ // Concurrent, where the kubectl path was one spawn after another (#1201/#1209).
262
+ await Promise.all(reads);
263
+ return observation(result, unobserved);
264
+ }
package/dist/detect.js ADDED
@@ -0,0 +1,16 @@
1
+ /** Edge-safe template detection for gcp (Config Connector) — see #426. */
2
+ export function detectTemplate(data) {
3
+ // Handle raw string input (unparsed YAML)
4
+ if (typeof data === "string") {
5
+ return data.includes("cnrm.cloud.google.com");
6
+ }
7
+ // Handle parsed YAML objects
8
+ if (typeof data !== "object" || data === null)
9
+ return false;
10
+ const obj = data;
11
+ const apiVersion = obj.apiVersion;
12
+ if (typeof apiVersion === "string" && apiVersion.includes("cnrm.cloud.google.com")) {
13
+ return true;
14
+ }
15
+ return false;
16
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Live export for the GCP Config Connector lexicon — implements
3
+ * LexiconPlugin.exportResources() so `chant import --from <gcp-env>`
4
+ * regenerates GCP resources as chant TypeScript.
5
+ *
6
+ * Config Connector encodes each GCP resource as a Kubernetes CRD on a
7
+ * management cluster, so export reads live CC objects with kubectl and maps
8
+ * them to the import IR. A type selector targets one kind; otherwise every
9
+ * `*.cnrm.cloud.google.com` resource kind is discovered and swept. All I/O
10
+ * lives here; cleaning and IR-building is pure in `./import/live-export`.
11
+ */
12
+ import { exec } from "node:child_process";
13
+ import { promisify } from "node:util";
14
+ import { deriveGVK } from "./describe-resources.js";
15
+ import { buildExportFromObjects } from "./import/live-export.js";
16
+ const execAsync = promisify(exec);
17
+ /** Discover all Config Connector resource kinds present in the cluster. */
18
+ async function discoverCnrmResources() {
19
+ try {
20
+ const { stdout } = await execAsync("kubectl api-resources -o name");
21
+ return stdout
22
+ .split("\n")
23
+ .map((l) => l.trim())
24
+ .filter((l) => l.endsWith(".cnrm.cloud.google.com"));
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ export async function exportResources(options) {
31
+ let resources;
32
+ if (options.selector?.type) {
33
+ const gvk = deriveGVK(options.selector.type);
34
+ resources = gvk ? [`${gvk.kind.toLowerCase()}.${gvk.group}`] : [];
35
+ }
36
+ else {
37
+ resources = await discoverCnrmResources();
38
+ }
39
+ if (resources.length === 0) {
40
+ return { resources: [], parameters: [] };
41
+ }
42
+ const objects = [];
43
+ for (const resource of resources) {
44
+ try {
45
+ const { stdout } = await execAsync(["kubectl", "get", resource, "-A", "-o", "json"].join(" "));
46
+ const list = JSON.parse(stdout);
47
+ if (Array.isArray(list.items))
48
+ objects.push(...list.items);
49
+ }
50
+ catch {
51
+ // Kind absent / RBAC denied — skip, don't fail the whole export.
52
+ }
53
+ }
54
+ return buildExportFromObjects(objects, {
55
+ verbatim: options.verbatim,
56
+ selector: options.selector,
57
+ owned: options.owned,
58
+ });
59
+ }