@intentius/chant-lexicon-gcp 0.39.0 → 0.41.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "99b2ae36e1f0b8a8630ca7ca10b22d68fd9ad271939798bf7f39948a1b581d39",
4
+ "manifest.json": "7f3bd8b2a8cc81d44953c0e383b432c07b72f5d75405fc7a15d59736f39c7bee",
5
5
  "meta.json": "475f10909854d059a2e2d4fb541c058803422e38b9c64bd4d6858c0623eedb8c",
6
6
  "types/index.d.ts": "5433b54c04b57d6fda8f3d81ef92ed0aca87f7e8e5dd3ea92d8de6e62a3ccfab",
7
7
  "rules/hardcoded-project.ts": "228631d3159e1ffcce2359c66073d4ae59bb0285f378e46e446f416aec50481c",
@@ -37,5 +37,5 @@
37
37
  "skills/chant-gcp-patterns.md": "a7ef31c1eb2f7244d3f73952c300472ef94c1eb09bd7a1003281b89299b6b704",
38
38
  "skills/chant-gcp-gke.md": "be277019da9a722c851e47cd2dfb9c9536668948c3535fb20db7697e934c4e2b"
39
39
  },
40
- "composite": "dd43cae524aba69f7713ccfd5f43c6c40a66d0a2d3656d99366335b550485964"
40
+ "composite": "be374ea7036813fbbfde4ff5d70f9797355c64073d13fdc67c68d7ca805479e4"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcp",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GCP",
6
6
  "intrinsics": [],
@@ -1,6 +1,22 @@
1
- /** The GCP labels chant stamps on resources it creates. */
1
+ import { type ApplyResult } from "@intentius/chant/apply";
2
+ /**
3
+ * The GCP labels chant stamps on resources it creates.
4
+ *
5
+ * Only the managed-by marker, not stack/env: this applier has no ownership
6
+ * marker to hand — the serializer stamps those from project config onto the
7
+ * Config Connector object. Same shape the fly serializer uses when no ownership
8
+ * context is set.
9
+ */
2
10
  export declare function chantOwnershipLabels(): Record<string, string>;
3
- /** True when a live resource's labels carry chant's ownership marker. */
11
+ /**
12
+ * True when a live resource's labels carry chant's ownership marker.
13
+ *
14
+ * Resolves through core's `hasOwnershipMarker` against the lexicon's declared
15
+ * GCP-resource channel (#1446) rather than comparing a string literal inline,
16
+ * so the stamp and the prune filter cannot drift. The key itself is unchanged
17
+ * and deliberately GCP-valid — see `../../ownership.ts` for why this surface
18
+ * cannot use core's `LABEL_OWNERSHIP_KEYS`.
19
+ */
4
20
  export declare function isChantOwned(labels: Record<string, string> | null | undefined): boolean;
5
21
  /** Minimal shape of a serialized CNRM resource (`*.cnrm.cloud.google.com`). */
6
22
  export interface GcpResource {
@@ -215,26 +231,94 @@ export interface GcpApplyArgs {
215
231
  */
216
232
  prune?: boolean;
217
233
  }
234
+ /**
235
+ * Normalize a gcp apply/delete result into core's apply envelope (#1446).
236
+ *
237
+ * The lexicon keeps its own richer shape — `created`/`updated` per resource,
238
+ * the CNRM `kind` — and this projects it onto the shared tri-state so a caller
239
+ * can read any applier's result the same way. Core's contract is the interface;
240
+ * it is not a replacement for what this applier knows.
241
+ */
242
+ export declare function toApplyResult(result: {
243
+ applied?: Array<{
244
+ kind: string;
245
+ name: string;
246
+ created: boolean;
247
+ updated: boolean;
248
+ }>;
249
+ deleted?: Array<{
250
+ kind: string;
251
+ name: string;
252
+ deleted: boolean;
253
+ }>;
254
+ pruned?: Array<{
255
+ kind: string;
256
+ name: string;
257
+ deleted: boolean;
258
+ }>;
259
+ notAttempted?: GcpNotAttempted[];
260
+ notPrunable?: GcpNotPrunable[];
261
+ }): ApplyResult;
262
+ /**
263
+ * A resource this run did not attempt, and why (#1447).
264
+ *
265
+ * The read path learned this in #1089/#1201: a read that never happened is not
266
+ * the same fact as a resource that is not there, and collapsing the two made
267
+ * "absent" mean both. `observation.ts` puts it as "a warn on stderr is not a
268
+ * signal in a change set" — and a `console.log` on stdout is not a signal in an
269
+ * apply result, for the same reason. So the skips ride the return value.
270
+ */
271
+ export interface GcpNotAttempted {
272
+ kind: string;
273
+ name: string;
274
+ /** `MAPPERS` has no entry for this kind, so no REST call exists to make. */
275
+ reason: "unsupported-kind";
276
+ }
277
+ /** A kind an owned-only prune could not consider, and why (#1447). */
278
+ export interface GcpNotPrunable {
279
+ kind: string;
280
+ /**
281
+ * - `no-list-capability` — the mapper cannot enumerate live resources of this
282
+ * kind, so an owned orphan of it is never even a delete candidate.
283
+ * - `list-failed` — the list call itself failed. Transient or not, it means
284
+ * this kind was not considered, which `pruned: []` alone would report as
285
+ * "nothing to prune".
286
+ */
287
+ reason: "no-list-capability" | "list-failed";
288
+ /** Status and body for `list-failed`, so the caller can tell apart a 403 from a 500. */
289
+ detail?: string;
290
+ }
218
291
  /**
219
292
  * Owned-only prune: for each manifested kind with `list` support, delete the
220
293
  * chant-owned live resources whose name is not in `desiredByKind`. Foreign
221
294
  * resources (no ownership label) are left alone.
295
+ *
296
+ * Returns what it could not consider alongside what it deleted — a kind with no
297
+ * `list`, or whose list call failed, is reported rather than dropped (#1447).
222
298
  */
223
299
  export declare function pruneOrphans(desired: GcpResource[], resolve: (mapper: ResourceMapper, resource?: GcpResource) => {
224
300
  base: string;
225
301
  project: string;
226
- }, http?: GcpHttp, signal?: AbortSignal): Promise<Array<{
227
- kind: string;
228
- name: string;
229
- deleted: boolean;
230
- }>>;
302
+ }, http?: GcpHttp, signal?: AbortSignal): Promise<{
303
+ pruned: Array<{
304
+ kind: string;
305
+ name: string;
306
+ deleted: boolean;
307
+ }>;
308
+ notPrunable: GcpNotPrunable[];
309
+ }>;
231
310
  /**
232
311
  * The native GCP applier (#706) — read a built CNRM manifest and apply each
233
312
  * resource directly to its GCP REST API, targeting a local floci-gcp emulator or
234
313
  * real GCP by endpoint override. Unlike AWS/Azure/k8s, GCP has no native deploy
235
314
  * service to shell out to, so chant maps each `kind` to a REST call itself (the
236
- * `MAPPERS` dispatch table). Unknown kinds are skipped. Uses longInfra profile.
237
- * `http` is injectable for tests.
315
+ * `MAPPERS` dispatch table). Uses longInfra profile. `http` is injectable for
316
+ * tests.
317
+ *
318
+ * A kind `MAPPERS` does not cover is **reported, not dropped** — it comes back
319
+ * in `notAttempted` (#1447). Before that, it was a `console.log` and a
320
+ * populated `applied` array, so a caller could not tell a partial apply from a
321
+ * complete one and `ApplyOp` reported success either way.
238
322
  */
239
323
  export declare function gcpApply(args: GcpApplyArgs, signal?: AbortSignal, http?: GcpHttp): Promise<{
240
324
  applied: Array<{
@@ -248,6 +332,10 @@ export declare function gcpApply(args: GcpApplyArgs, signal?: AbortSignal, http?
248
332
  name: string;
249
333
  deleted: boolean;
250
334
  }>;
335
+ /** Declared resources no REST call was made for. Empty on a complete apply. */
336
+ notAttempted: GcpNotAttempted[];
337
+ /** Kinds the prune could not consider. Empty when `prune` is off. */
338
+ notPrunable: GcpNotPrunable[];
251
339
  }>;
252
340
  /**
253
341
  * Idempotently delete one resource: `DELETE` its resource URL. A 404 means it is
@@ -274,5 +362,9 @@ export declare function gcpDelete(args: GcpApplyArgs, signal?: AbortSignal, http
274
362
  name: string;
275
363
  deleted: boolean;
276
364
  }>;
365
+ /** Declared resources no delete was attempted for — the delete-side twin of
366
+ * `gcpApply`'s field, and the more consequential one: a caller reading only
367
+ * `deleted` would believe the manifest was fully torn down (#1447). */
368
+ notAttempted: GcpNotAttempted[];
277
369
  }>;
278
370
  //# sourceMappingURL=gcp-apply.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gcp-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/gcp-apply.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gcp-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/gcp-apply.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  * (`gcpApply`), which maps CNRM resources to GCP REST calls since GCP has no
5
5
  * native deployment service to shell out to.
6
6
  */
7
- export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, chantOwnershipLabels, isChantOwned, resolveGcpProject, parseManifest, } from "./gcp-apply.js";
7
+ export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, toApplyResult, type GcpNotAttempted, type GcpNotPrunable, chantOwnershipLabels, isChantOwned, resolveGcpProject, parseManifest, } from "./gcp-apply.js";
8
8
  export type { GcpApplyArgs, GcpResource, CnrmStorageBucket, BucketInsertBody, PubSubTopicBody, ResourceMapper, OperationSpec, ListSpec, ApplyPlan, GcpHttp, } from "./gcp-apply.js";
9
9
  export { flociGcpUp, flociGcpDown, flociGcpRunCommand, flociGcpRmCommand, flociGcpExistsCommand, flociGcpHealthUrl, flociGcpEndpoint, } from "./floci-gcp.js";
10
10
  export type { FlociGcpUpArgs, FlociGcpDownArgs } from "./floci-gcp.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACR,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACR,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GCP's ownership marker channels (#1446).
3
+ *
4
+ * The gcp lexicon marks ownership on **two different surfaces**, and they
5
+ * legitimately use different key vocabularies:
6
+ *
7
+ * 1. **The Config Connector object** — what the serializer emits. Its
8
+ * `metadata.labels` are Kubernetes labels, so it uses core's shared
9
+ * `LABEL_OWNERSHIP_KEYS` (`app.kubernetes.io/managed-by`). That is the key
10
+ * `plugin.ts` registers as the lexicon's `ownershipChannel` and the one the
11
+ * read paths resolve through.
12
+ *
13
+ * 2. **The GCP resource itself** — what `gcpApply` PUTs to the REST API. GCP
14
+ * label keys may not contain `/` or `.`, so `app.kubernetes.io/managed-by`
15
+ * is not a legal key there. Hence {@link GCP_RESOURCE_OWNERSHIP_KEYS}.
16
+ *
17
+ * This is worth stating because the split looks like the bug #1446 found in the
18
+ * azure applier, and is not. Azure's serializer and applier both write ARM tags
19
+ * — one surface, and they had drifted onto two keys for no reason. Here the two
20
+ * surfaces are real and each key is the only legal one for its target.
21
+ *
22
+ * What #1446 changes is that the applier's predicate resolves through core's
23
+ * `hasOwnershipMarker` against a declared channel, rather than comparing a
24
+ * string literal inline. The key does not move; the drift risk does.
25
+ */
26
+ import type { ChannelKeys } from "@intentius/chant/ownership";
27
+ /**
28
+ * Ownership keys for labels on a live GCP resource — the surface `gcpApply`
29
+ * writes and `pruneOrphans` reads.
30
+ *
31
+ * GCP label keys are lowercase alphanumerics, `-` and `_` only, so this is the
32
+ * GCP-valid equivalent of core's label convention rather than that convention
33
+ * itself.
34
+ */
35
+ export declare const GCP_RESOURCE_OWNERSHIP_KEYS: ChannelKeys;
36
+ //# sourceMappingURL=ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,EAAE,WAIzC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.39.0",
3
+ "version": "0.41.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.39.0",
79
+ "@intentius/chant": "^0.41.0",
80
80
  "typescript": "^5.9.3"
81
81
  }
82
82
  }
@@ -1,4 +1,6 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { writeFileSync, unlinkSync } from "node:fs";
3
+ import { describeApplyConformance } from "@intentius/chant-test-utils";
2
4
  import {
3
5
  bucketInsertBody,
4
6
  pubSubTopicBody,
@@ -12,6 +14,9 @@ import {
12
14
  referencedNames,
13
15
  orderByReferences,
14
16
  pruneOrphans,
17
+ gcpApply,
18
+ gcpDelete,
19
+ toApplyResult,
15
20
  chantOwnershipLabels,
16
21
  isChantOwned,
17
22
  pubSubSubscriptionBody,
@@ -454,12 +459,47 @@ describe("ownership + prune (#706)", () => {
454
459
  return { status: 200, text: "" }; // DELETE
455
460
  };
456
461
  const resolve = () => ({ base: "http://x", project: "p" });
457
- const pruned = await pruneOrphans(desired, resolve, http);
462
+ const { pruned, notPrunable } = await pruneOrphans(desired, resolve, http);
458
463
  expect(pruned).toEqual([{ kind: "StorageBucket", name: "orphan", deleted: true }]);
464
+ expect(notPrunable).toEqual([]);
459
465
  expect(calls.filter((c) => c.method === "DELETE")).toEqual([
460
466
  { method: "DELETE", url: "http://x/storage/v1/b/orphan" },
461
467
  ]);
462
468
  });
469
+
470
+ // #1447. A kind the prune could not consider was `continue`, so `pruned: []`
471
+ // came back — which reads as "there was nothing to prune", not "I did not
472
+ // look at this kind". Same conflation the read path fixed in #1089/#1201.
473
+ test("a kind with no list capability is reported, not silently skipped", async () => {
474
+ // IAMServiceAccount is mapped but has no `list` spec.
475
+ const desired: GcpResource[] = [{ kind: "IAMServiceAccount", metadata: { name: "sa" } }];
476
+ const calls: string[] = [];
477
+ const http: GcpHttp = async (method) => {
478
+ calls.push(method);
479
+ return { status: 200, text: "{}" };
480
+ };
481
+ const { pruned, notPrunable } = await pruneOrphans(desired, () => ({ base: "http://x", project: "p" }), http);
482
+ expect(pruned).toEqual([]);
483
+ expect(notPrunable).toEqual([{ kind: "IAMServiceAccount", reason: "no-list-capability" }]);
484
+ // Reported without touching the transport at all.
485
+ expect(calls).toEqual([]);
486
+ });
487
+
488
+ // Not in #1447's list, same class: a failed LIST was also `continue`. A 403 or
489
+ // a 500 meant the kind went unconsidered and the result said nothing.
490
+ test("a failed list is reported, with the status, not silently skipped", async () => {
491
+ const desired: GcpResource[] = [{ kind: "StorageBucket", metadata: { name: "keep" } }];
492
+ const deletes: string[] = [];
493
+ const http: GcpHttp = async (method, url) => {
494
+ if (method === "DELETE") deletes.push(url);
495
+ return method === "GET" ? { status: 403, text: "forbidden" } : { status: 200, text: "" };
496
+ };
497
+ const { pruned, notPrunable } = await pruneOrphans(desired, () => ({ base: "http://x", project: "p" }), http);
498
+ expect(pruned).toEqual([]);
499
+ expect(notPrunable).toEqual([{ kind: "StorageBucket", reason: "list-failed", detail: "403: forbidden" }]);
500
+ // And nothing was deleted on the strength of a list that failed.
501
+ expect(deletes).toEqual([]);
502
+ });
463
503
  });
464
504
 
465
505
  describe("parseManifest (#711)", () => {
@@ -473,3 +513,196 @@ describe("parseManifest (#711)", () => {
473
513
  expect(docs.map((d) => d.kind)).toEqual(["StorageBucket", "PubSubTopic"]);
474
514
  });
475
515
  });
516
+
517
+ /**
518
+ * #1447 — a resource `MAPPERS` has no entry for was dropped with a
519
+ * `console.log`, and the return value looked exactly like a complete apply.
520
+ * `ApplyOp` reported success over a partial one.
521
+ *
522
+ * The GCP lexicon serializes many more kinds than the six `MAPPERS` covers, so
523
+ * this is not a hypothetical: anything else in the manifest was silently not
524
+ * applied.
525
+ */
526
+ describe("gcpApply / gcpDelete report what they did not attempt (#1447)", () => {
527
+ /** A manifest with one kind chant can apply and one it cannot. */
528
+ const manifest = (): string => {
529
+ const path = `/tmp/chant-1447-${process.pid}-${Math.random().toString(36).slice(2)}.json`;
530
+ writeFileSync(
531
+ path,
532
+ JSON.stringify([
533
+ { kind: "StorageBucket", metadata: { name: "mapped" }, spec: { location: "US" } },
534
+ { kind: "SQLInstance", metadata: { name: "unmapped" }, spec: {} },
535
+ ]),
536
+ );
537
+ return path;
538
+ };
539
+
540
+ const ok: GcpHttp = async (method) =>
541
+ method === "GET" ? { status: 404, text: "" } : { status: 200, text: "{}" };
542
+
543
+ test("an unmapped kind lands in notAttempted, not in applied", async () => {
544
+ const path = manifest();
545
+ try {
546
+ const res = await gcpApply({ manifestPath: path, endpoint: "http://x", project: "p" }, undefined, ok);
547
+ expect(res.applied.map((a) => a.name)).toEqual(["mapped"]);
548
+ expect(res.notAttempted).toEqual([
549
+ { kind: "SQLInstance", name: "unmapped", reason: "unsupported-kind" },
550
+ ]);
551
+ } finally {
552
+ unlinkSync(path);
553
+ }
554
+ });
555
+
556
+ // The distinction the old shape could not express: without notAttempted, this
557
+ // result and one from a manifest that never declared SQLInstance are identical.
558
+ test("a fully-applied manifest reports notAttempted empty", async () => {
559
+ const path = `/tmp/chant-1447-full-${process.pid}.json`;
560
+ writeFileSync(path, JSON.stringify([{ kind: "StorageBucket", metadata: { name: "only" }, spec: {} }]));
561
+ try {
562
+ const res = await gcpApply({ manifestPath: path, endpoint: "http://x", project: "p" }, undefined, ok);
563
+ expect(res.applied).toHaveLength(1);
564
+ expect(res.notAttempted).toEqual([]);
565
+ expect(res.notPrunable).toEqual([]);
566
+ } finally {
567
+ unlinkSync(path);
568
+ }
569
+ });
570
+
571
+ // The delete side matters more: a caller reading only `deleted` would believe
572
+ // the manifest was fully torn down.
573
+ test("gcpDelete reports the resources it never attempted to delete", async () => {
574
+ const path = manifest();
575
+ try {
576
+ const res = await gcpDelete({ manifestPath: path, endpoint: "http://x", project: "p" }, undefined, ok);
577
+ expect(res.deleted.map((d) => d.name)).toEqual(["mapped"]);
578
+ expect(res.notAttempted).toEqual([
579
+ { kind: "SQLInstance", name: "unmapped", reason: "unsupported-kind" },
580
+ ]);
581
+ } finally {
582
+ unlinkSync(path);
583
+ }
584
+ });
585
+
586
+ // A malformed manifest is a different fact from an unsupported kind — one is a
587
+ // bug in the input, the other a known gap in coverage. Only the second belongs
588
+ // in notAttempted.
589
+ test("a manifest entry with no kind throws instead of being skipped", async () => {
590
+ const path = `/tmp/chant-1447-nokind-${process.pid}.json`;
591
+ writeFileSync(path, JSON.stringify([{ metadata: { name: "kindless" } }]));
592
+ try {
593
+ await expect(
594
+ gcpApply({ manifestPath: path, endpoint: "http://x", project: "p" }, undefined, ok),
595
+ ).rejects.toThrow(/no "kind".*kindless.*malformed manifest/s);
596
+ } finally {
597
+ unlinkSync(path);
598
+ }
599
+ });
600
+ });
601
+
602
+ /**
603
+ * The shared apply-contract suite (#1446), run against gcp's own mocked
604
+ * transport. gcp is the reference adopter because #1447 already gave it the
605
+ * not-attempted facts; this proves the seam carries them.
606
+ *
607
+ * Assertion 3 ("accounts for every resource in the plan") is the one that fails
608
+ * on pre-#1447 gcp — an unmapped kind was in the plan and in no bucket.
609
+ */
610
+ describeApplyConformance({
611
+ lexicon: "gcp",
612
+ scenarios: [
613
+ {
614
+ name: "a manifest with one mapped and one unmapped kind",
615
+ plan: [
616
+ { kind: "StorageBucket", name: "mapped" },
617
+ { kind: "SQLInstance", name: "unmapped" },
618
+ ],
619
+ run: async () => {
620
+ const path = `/tmp/chant-1446-gcp-${process.pid}.json`;
621
+ writeFileSync(
622
+ path,
623
+ JSON.stringify([
624
+ { kind: "StorageBucket", metadata: { name: "mapped" }, spec: { location: "US" } },
625
+ { kind: "SQLInstance", metadata: { name: "unmapped" }, spec: {} },
626
+ ]),
627
+ );
628
+ try {
629
+ const http: GcpHttp = async (method) =>
630
+ method === "GET" ? { status: 404, text: "" } : { status: 200, text: "{}" };
631
+ return toApplyResult(
632
+ await gcpApply({ manifestPath: path, endpoint: "http://x", project: "p" }, undefined, http),
633
+ );
634
+ } finally {
635
+ unlinkSync(path);
636
+ }
637
+ },
638
+ expectApplied: ["StorageBucket/mapped"],
639
+ expectNotAttempted: ["SQLInstance/unmapped"],
640
+ },
641
+ ],
642
+ pruneScenarios: [
643
+ {
644
+ name: "an owned orphan beside a foreign resource in the same project",
645
+ ownedOrphan: "orphan",
646
+ foreign: "foreign",
647
+ run: async () => {
648
+ const deletes: string[] = [];
649
+ const live = {
650
+ items: [
651
+ { name: "keep", labels: { "managed-by": "chant" } },
652
+ { name: "orphan", labels: { "managed-by": "chant" } },
653
+ { name: "foreign", labels: { "managed-by": "terraform" } },
654
+ ],
655
+ };
656
+ const path = `/tmp/chant-1446-gcp-prune-${process.pid}.json`;
657
+ writeFileSync(
658
+ path,
659
+ JSON.stringify([{ kind: "StorageBucket", metadata: { name: "keep" }, spec: {} }]),
660
+ );
661
+ try {
662
+ const http: GcpHttp = async (method, url) => {
663
+ if (method === "DELETE") deletes.push(url);
664
+ if (method === "GET" && url.includes("/b?")) return { status: 200, text: JSON.stringify(live) };
665
+ if (method === "GET") return { status: 200, text: "{}" }; // `keep` already exists
666
+ return { status: 200, text: "{}" };
667
+ };
668
+ const result = toApplyResult(
669
+ await gcpApply(
670
+ { manifestPath: path, endpoint: "http://x", project: "p", prune: true },
671
+ undefined,
672
+ http,
673
+ ),
674
+ );
675
+ return { result, deletes };
676
+ } finally {
677
+ unlinkSync(path);
678
+ }
679
+ },
680
+ },
681
+ ],
682
+ idempotenceScenarios: [
683
+ {
684
+ name: "the same manifest applied twice",
685
+ run: async () => {
686
+ const path = `/tmp/chant-1446-gcp-idem-${process.pid}.json`;
687
+ writeFileSync(
688
+ path,
689
+ JSON.stringify([{ kind: "StorageBucket", metadata: { name: "b" }, spec: { location: "US" } }]),
690
+ );
691
+ try {
692
+ let exists = false;
693
+ const http: GcpHttp = async (method) => {
694
+ if (method === "GET") return exists ? { status: 200, text: "{}" } : { status: 404, text: "" };
695
+ exists = true;
696
+ return { status: 200, text: "{}" };
697
+ };
698
+ const args = { manifestPath: path, endpoint: "http://x", project: "p" };
699
+ const first = toApplyResult(await gcpApply(args, undefined, http));
700
+ const second = toApplyResult(await gcpApply(args, undefined, http));
701
+ return { first, second };
702
+ } finally {
703
+ unlinkSync(path);
704
+ }
705
+ },
706
+ },
707
+ ],
708
+ });
@@ -1,23 +1,40 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { parseYAML } from "@intentius/chant/yaml";
3
3
  import { safeHeartbeat, sleep } from "@intentius/chant/op";
4
+ import { hasOwnershipMarker, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
5
+ import { GCP_RESOURCE_OWNERSHIP_KEYS } from "../../ownership";
6
+ import {
7
+ applyResult,
8
+ type ApplyResult,
9
+ type AppliedResource,
10
+ type NotAttemptedResource,
11
+ } from "@intentius/chant/apply";
4
12
 
5
13
  const PROJECT_ID_ANNOTATION = "cnrm.cloud.google.com/project-id";
6
14
 
7
- // Ownership marker stamped as a GCP resource label so `prune` can identify what
8
- // chant created (GCP label keys can't hold the k8s `app.kubernetes.io/managed-by`
9
- // slash/dot form, so this is the GCP-valid equivalent).
10
- const OWNERSHIP_LABEL_KEY = "managed-by";
11
- const OWNERSHIP_LABEL_VALUE = "chant";
12
-
13
- /** The GCP labels chant stamps on resources it creates. */
15
+ /**
16
+ * The GCP labels chant stamps on resources it creates.
17
+ *
18
+ * Only the managed-by marker, not stack/env: this applier has no ownership
19
+ * marker to hand — the serializer stamps those from project config onto the
20
+ * Config Connector object. Same shape the fly serializer uses when no ownership
21
+ * context is set.
22
+ */
14
23
  export function chantOwnershipLabels(): Record<string, string> {
15
- return { [OWNERSHIP_LABEL_KEY]: OWNERSHIP_LABEL_VALUE };
24
+ return { [GCP_RESOURCE_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE };
16
25
  }
17
26
 
18
- /** True when a live resource's labels carry chant's ownership marker. */
27
+ /**
28
+ * True when a live resource's labels carry chant's ownership marker.
29
+ *
30
+ * Resolves through core's `hasOwnershipMarker` against the lexicon's declared
31
+ * GCP-resource channel (#1446) rather than comparing a string literal inline,
32
+ * so the stamp and the prune filter cannot drift. The key itself is unchanged
33
+ * and deliberately GCP-valid — see `../../ownership.ts` for why this surface
34
+ * cannot use core's `LABEL_OWNERSHIP_KEYS`.
35
+ */
19
36
  export function isChantOwned(labels: Record<string, string> | null | undefined): boolean {
20
- return labels?.[OWNERSHIP_LABEL_KEY] === OWNERSHIP_LABEL_VALUE;
37
+ return hasOwnershipMarker(labels ?? undefined, GCP_RESOURCE_OWNERSHIP_KEYS);
21
38
  }
22
39
 
23
40
  /** Merge the ownership marker into a create/update body's `labels`. */
@@ -533,6 +550,25 @@ export async function waitForOperation(
533
550
  throw new Error(`operation did not complete within timeout: ${pollUrl}`);
534
551
  }
535
552
 
553
+ /**
554
+ * A manifest entry with no `kind` is malformed, not unsupported (#1447).
555
+ *
556
+ * The old guard skipped it in silence, which put a hand-written or truncated
557
+ * manifest in the same bucket as a kind chant simply has no mapper for. One is
558
+ * a bug in the input and the other is a known gap in coverage; only the second
559
+ * belongs in `notAttempted`. Throws so the first surfaces.
560
+ */
561
+ function requireKind(resource: GcpResource, manifestPath: string): boolean {
562
+ if (!resource.kind) {
563
+ const name = resource.metadata?.name;
564
+ throw new Error(
565
+ `${manifestPath}: manifest entry has no "kind"${name ? ` (metadata.name: ${name})` : ""} — ` +
566
+ `a CNRM resource must declare one. This is a malformed manifest, not an unsupported kind.`,
567
+ );
568
+ }
569
+ return true;
570
+ }
571
+
536
572
  /** Parse a built manifest into CNRM resource objects — YAML (multi-doc) or JSON. Pure. */
537
573
  export function parseManifest(content: string, path: string): GcpResource[] {
538
574
  if (/\.json$/i.test(path)) {
@@ -561,17 +597,90 @@ export interface GcpApplyArgs {
561
597
  prune?: boolean;
562
598
  }
563
599
 
600
+ /**
601
+ * Normalize a gcp apply/delete result into core's apply envelope (#1446).
602
+ *
603
+ * The lexicon keeps its own richer shape — `created`/`updated` per resource,
604
+ * the CNRM `kind` — and this projects it onto the shared tri-state so a caller
605
+ * can read any applier's result the same way. Core's contract is the interface;
606
+ * it is not a replacement for what this applier knows.
607
+ */
608
+ export function toApplyResult(result: {
609
+ applied?: Array<{ kind: string; name: string; created: boolean; updated: boolean }>;
610
+ deleted?: Array<{ kind: string; name: string; deleted: boolean }>;
611
+ pruned?: Array<{ kind: string; name: string; deleted: boolean }>;
612
+ notAttempted?: GcpNotAttempted[];
613
+ notPrunable?: GcpNotPrunable[];
614
+ }): ApplyResult {
615
+ const applied: AppliedResource[] = (result.applied ?? []).map((a) => ({
616
+ kind: a.kind,
617
+ name: a.name,
618
+ action: a.created ? "created" : a.updated ? "updated" : "unchanged",
619
+ }));
620
+ const notAttempted: NotAttemptedResource[] = [
621
+ ...(result.notAttempted ?? []).map((n) => ({ kind: n.kind, name: n.name, reason: n.reason })),
622
+ // A kind the prune could not consider has no single resource name — the
623
+ // whole kind went unexamined, which `not-prunable` is the reason for.
624
+ ...(result.notPrunable ?? []).map((n) => ({
625
+ kind: n.kind,
626
+ name: "*",
627
+ reason: "not-prunable" as const,
628
+ ...(n.detail ? { detail: n.detail } : {}),
629
+ })),
630
+ ];
631
+ // `gcpDelete` reports deletions as `deleted`; they are prunes in the shared
632
+ // vocabulary — the applier removed something the plan no longer wants.
633
+ return applyResult(applied, result.pruned ?? result.deleted ?? [], notAttempted);
634
+ }
635
+
636
+ /**
637
+ * A resource this run did not attempt, and why (#1447).
638
+ *
639
+ * The read path learned this in #1089/#1201: a read that never happened is not
640
+ * the same fact as a resource that is not there, and collapsing the two made
641
+ * "absent" mean both. `observation.ts` puts it as "a warn on stderr is not a
642
+ * signal in a change set" — and a `console.log` on stdout is not a signal in an
643
+ * apply result, for the same reason. So the skips ride the return value.
644
+ */
645
+ export interface GcpNotAttempted {
646
+ kind: string;
647
+ name: string;
648
+ /** `MAPPERS` has no entry for this kind, so no REST call exists to make. */
649
+ reason: "unsupported-kind";
650
+ }
651
+
652
+ /** A kind an owned-only prune could not consider, and why (#1447). */
653
+ export interface GcpNotPrunable {
654
+ kind: string;
655
+ /**
656
+ * - `no-list-capability` — the mapper cannot enumerate live resources of this
657
+ * kind, so an owned orphan of it is never even a delete candidate.
658
+ * - `list-failed` — the list call itself failed. Transient or not, it means
659
+ * this kind was not considered, which `pruned: []` alone would report as
660
+ * "nothing to prune".
661
+ */
662
+ reason: "no-list-capability" | "list-failed";
663
+ /** Status and body for `list-failed`, so the caller can tell apart a 403 from a 500. */
664
+ detail?: string;
665
+ }
666
+
564
667
  /**
565
668
  * Owned-only prune: for each manifested kind with `list` support, delete the
566
669
  * chant-owned live resources whose name is not in `desiredByKind`. Foreign
567
670
  * resources (no ownership label) are left alone.
671
+ *
672
+ * Returns what it could not consider alongside what it deleted — a kind with no
673
+ * `list`, or whose list call failed, is reported rather than dropped (#1447).
568
674
  */
569
675
  export async function pruneOrphans(
570
676
  desired: GcpResource[],
571
677
  resolve: (mapper: ResourceMapper, resource?: GcpResource) => { base: string; project: string },
572
678
  http: GcpHttp = defaultHttp,
573
679
  signal?: AbortSignal,
574
- ): Promise<Array<{ kind: string; name: string; deleted: boolean }>> {
680
+ ): Promise<{
681
+ pruned: Array<{ kind: string; name: string; deleted: boolean }>;
682
+ notPrunable: GcpNotPrunable[];
683
+ }> {
575
684
  const desiredByKind = new Map<string, Set<string>>();
576
685
  for (const r of desired) {
577
686
  if (!r.kind) continue;
@@ -581,12 +690,21 @@ export async function pruneOrphans(
581
690
  }
582
691
 
583
692
  const pruned: Array<{ kind: string; name: string; deleted: boolean }> = [];
693
+ const notPrunable: GcpNotPrunable[] = [];
584
694
  for (const [kind, keep] of desiredByKind) {
585
695
  const mapper = MAPPERS[kind];
586
- if (!mapper?.list) continue;
696
+ if (!mapper?.list) {
697
+ console.log(`prune: cannot list kind ${kind} — owned orphans of it are not considered`);
698
+ notPrunable.push({ kind, reason: "no-list-capability" });
699
+ continue;
700
+ }
587
701
  const ctx = resolve(mapper, desired.find((r) => r.kind === kind));
588
702
  const res = await http("GET", mapper.list.url(ctx), undefined, signal);
589
- if (res.status >= 300) continue;
703
+ if (res.status >= 300) {
704
+ console.log(`prune: listing kind ${kind} failed (${res.status}) — not considered`);
705
+ notPrunable.push({ kind, reason: "list-failed", detail: `${res.status}: ${res.text}` });
706
+ continue;
707
+ }
590
708
  for (const item of mapper.list.items(parseJson(res.text))) {
591
709
  if (!isChantOwned(item.labels) || keep.has(item.name)) continue;
592
710
  safeHeartbeat({ step: "prune", kind, name: item.name });
@@ -595,7 +713,7 @@ export async function pruneOrphans(
595
713
  pruned.push(result);
596
714
  }
597
715
  }
598
- return pruned;
716
+ return { pruned, notPrunable };
599
717
  }
600
718
 
601
719
  /**
@@ -603,8 +721,13 @@ export async function pruneOrphans(
603
721
  * resource directly to its GCP REST API, targeting a local floci-gcp emulator or
604
722
  * real GCP by endpoint override. Unlike AWS/Azure/k8s, GCP has no native deploy
605
723
  * service to shell out to, so chant maps each `kind` to a REST call itself (the
606
- * `MAPPERS` dispatch table). Unknown kinds are skipped. Uses longInfra profile.
607
- * `http` is injectable for tests.
724
+ * `MAPPERS` dispatch table). Uses longInfra profile. `http` is injectable for
725
+ * tests.
726
+ *
727
+ * A kind `MAPPERS` does not cover is **reported, not dropped** — it comes back
728
+ * in `notAttempted` (#1447). Before that, it was a `console.log` and a
729
+ * populated `applied` array, so a caller could not tell a partial apply from a
730
+ * complete one and `ApplyOp` reported success either way.
608
731
  */
609
732
  export async function gcpApply(
610
733
  args: GcpApplyArgs,
@@ -613,6 +736,10 @@ export async function gcpApply(
613
736
  ): Promise<{
614
737
  applied: Array<{ kind: string; name: string; created: boolean; updated: boolean }>;
615
738
  pruned: Array<{ kind: string; name: string; deleted: boolean }>;
739
+ /** Declared resources no REST call was made for. Empty on a complete apply. */
740
+ notAttempted: GcpNotAttempted[];
741
+ /** Kinds the prune could not consider. Empty when `prune` is off. */
742
+ notPrunable: GcpNotPrunable[];
616
743
  }> {
617
744
  const resources = orderByReferences(parseManifest(readFileSync(args.manifestPath, "utf8"), args.manifestPath));
618
745
  const resolve = (mapper: ResourceMapper, resource?: GcpResource) => ({
@@ -621,10 +748,13 @@ export async function gcpApply(
621
748
  });
622
749
 
623
750
  const applied: Array<{ kind: string; name: string; created: boolean; updated: boolean }> = [];
751
+ const notAttempted: GcpNotAttempted[] = [];
624
752
  for (const r of resources) {
625
- const mapper = r.kind ? MAPPERS[r.kind] : undefined;
753
+ const mapper = requireKind(r, args.manifestPath) ? MAPPERS[r.kind as string] : undefined;
626
754
  if (!mapper) {
627
- if (r.kind) console.log(`skip: no mapper for kind ${r.kind}`);
755
+ const kind = r.kind as string;
756
+ console.log(`skip: no mapper for kind ${kind}`);
757
+ notAttempted.push({ kind, name: r.metadata?.name ?? "?", reason: "unsupported-kind" });
628
758
  continue;
629
759
  }
630
760
  const ctx = resolve(mapper, r);
@@ -635,8 +765,10 @@ export async function gcpApply(
635
765
  applied.push(result);
636
766
  }
637
767
 
638
- const pruned = args.prune ? await pruneOrphans(resources, resolve, http, signal) : [];
639
- return { applied, pruned };
768
+ const prune = args.prune
769
+ ? await pruneOrphans(resources, resolve, http, signal)
770
+ : { pruned: [], notPrunable: [] };
771
+ return { applied, pruned: prune.pruned, notAttempted, notPrunable: prune.notPrunable };
640
772
  }
641
773
 
642
774
  /**
@@ -679,15 +811,24 @@ export async function gcpDelete(
679
811
  args: GcpApplyArgs,
680
812
  signal?: AbortSignal,
681
813
  http: GcpHttp = defaultHttp,
682
- ): Promise<{ deleted: Array<{ kind: string; name: string; deleted: boolean }> }> {
814
+ ): Promise<{
815
+ deleted: Array<{ kind: string; name: string; deleted: boolean }>;
816
+ /** Declared resources no delete was attempted for — the delete-side twin of
817
+ * `gcpApply`'s field, and the more consequential one: a caller reading only
818
+ * `deleted` would believe the manifest was fully torn down (#1447). */
819
+ notAttempted: GcpNotAttempted[];
820
+ }> {
683
821
  // Delete in reverse dependency order: a referrer goes before the resource it
684
822
  // references.
685
823
  const resources = orderByReferences(parseManifest(readFileSync(args.manifestPath, "utf8"), args.manifestPath)).reverse();
686
824
  const deleted: Array<{ kind: string; name: string; deleted: boolean }> = [];
825
+ const notAttempted: GcpNotAttempted[] = [];
687
826
  for (const r of resources) {
688
- const mapper = r.kind ? MAPPERS[r.kind] : undefined;
827
+ const mapper = requireKind(r, args.manifestPath) ? MAPPERS[r.kind as string] : undefined;
689
828
  if (!mapper) {
690
- if (r.kind) console.log(`skip: no mapper for kind ${r.kind}`);
829
+ const kind = r.kind as string;
830
+ console.log(`skip: no mapper for kind ${kind}`);
831
+ notAttempted.push({ kind, name: r.metadata?.name ?? "?", reason: "unsupported-kind" });
691
832
  continue;
692
833
  }
693
834
  const base = (args.endpoint ?? mapper.defaultHost).replace(/\/$/, "");
@@ -697,5 +838,5 @@ export async function gcpDelete(
697
838
  console.log(`${result.deleted ? "deleted" : "absent"}: ${result.kind}/${result.name} (${base})`);
698
839
  deleted.push(result);
699
840
  }
700
- return { deleted };
841
+ return { deleted, notAttempted };
701
842
  }
@@ -27,6 +27,9 @@ export {
27
27
  referencedNames,
28
28
  orderByReferences,
29
29
  pruneOrphans,
30
+ toApplyResult,
31
+ type GcpNotAttempted,
32
+ type GcpNotPrunable,
30
33
  chantOwnershipLabels,
31
34
  isChantOwned,
32
35
  resolveGcpProject,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GCP's ownership marker channels (#1446).
3
+ *
4
+ * The gcp lexicon marks ownership on **two different surfaces**, and they
5
+ * legitimately use different key vocabularies:
6
+ *
7
+ * 1. **The Config Connector object** — what the serializer emits. Its
8
+ * `metadata.labels` are Kubernetes labels, so it uses core's shared
9
+ * `LABEL_OWNERSHIP_KEYS` (`app.kubernetes.io/managed-by`). That is the key
10
+ * `plugin.ts` registers as the lexicon's `ownershipChannel` and the one the
11
+ * read paths resolve through.
12
+ *
13
+ * 2. **The GCP resource itself** — what `gcpApply` PUTs to the REST API. GCP
14
+ * label keys may not contain `/` or `.`, so `app.kubernetes.io/managed-by`
15
+ * is not a legal key there. Hence {@link GCP_RESOURCE_OWNERSHIP_KEYS}.
16
+ *
17
+ * This is worth stating because the split looks like the bug #1446 found in the
18
+ * azure applier, and is not. Azure's serializer and applier both write ARM tags
19
+ * — one surface, and they had drifted onto two keys for no reason. Here the two
20
+ * surfaces are real and each key is the only legal one for its target.
21
+ *
22
+ * What #1446 changes is that the applier's predicate resolves through core's
23
+ * `hasOwnershipMarker` against a declared channel, rather than comparing a
24
+ * string literal inline. The key does not move; the drift risk does.
25
+ */
26
+
27
+ import type { ChannelKeys } from "@intentius/chant/ownership";
28
+
29
+ /**
30
+ * Ownership keys for labels on a live GCP resource — the surface `gcpApply`
31
+ * writes and `pruneOrphans` reads.
32
+ *
33
+ * GCP label keys are lowercase alphanumerics, `-` and `_` only, so this is the
34
+ * GCP-valid equivalent of core's label convention rather than that convention
35
+ * itself.
36
+ */
37
+ export const GCP_RESOURCE_OWNERSHIP_KEYS: ChannelKeys = {
38
+ managedBy: "managed-by",
39
+ stack: "chant-stack",
40
+ env: "chant-env",
41
+ };