@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.
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/gcp-apply.d.ts +101 -9
- package/dist/op/activities/gcp-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +36 -0
- package/dist/ownership.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/op/activities/gcp-apply.test.ts +234 -1
- package/src/op/activities/gcp-apply.ts +165 -24
- package/src/op/activities/index.ts +3 -0
- package/src/ownership.ts +41 -0
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
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": "
|
|
40
|
+
"composite": "be374ea7036813fbbfde4ff5d70f9797355c64073d13fdc67c68d7ca805479e4"
|
|
41
41
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
/**
|
|
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<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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).
|
|
237
|
-
*
|
|
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":"
|
|
1
|
+
{"version":3,"file":"gcp-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/gcp-apply.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,WAAW,EAGjB,MAAM,wBAAwB,CAAC;AAIhC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEvF;AAQD,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACtC,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,wBAAwB,CAAC,EAAE,OAAO,CAAC;QACnC,UAAU,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QACnC,aAAa,CAAC,EAAE,KAAK,CAAC;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC,CAAC;KAClG,CAAC;CACH;AAED,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE;QAAE,wBAAwB,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAAC;IACtE,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAClC,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CACvG;AAED,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAID;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/D,MAAM,CAAC,EAAE;QAAE,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAClE;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,CAAC,kBAAkB,EAAE,OAAO,EAAE,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACjG,8DAA8D;IAC9D,MAAM,CAAC,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC;IACxC,8EAA8E;IAC9E,KAAK,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CACnD;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,kCAAkC;IAClC,GAAG,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;IACpD,4EAA4E;IAC5E,KAAK,CAAC,YAAY,EAAE,OAAO,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC/F;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC/E,iFAAiF;IACjF,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,oFAAoF;IACpF,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAcnE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,CAsB1E;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,eAAe,CAOtE;AAED,eAAO,MAAM,mBAAmB,EAAE,cAmBjC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,cA0B/B,CAAC;AAEF,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED,eAAO,MAAM,qBAAqB,EAAE,cAuBnC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtG;AAED,eAAO,MAAM,wBAAwB,EAAE,cAStC,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzE;AAED,eAAO,MAAM,yBAAyB,EAAE,cAuBvC,CAAC;AAEF,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGpG;AAED,eAAO,MAAM,uBAAuB,EAAE,cAerC,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAOlD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,EAAE,CAiB/D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAuBzE;AAID,kGAAkG;AAClG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAQrG;AAED,6FAA6F;AAC7F,MAAM,MAAM,OAAO,GAAG,CACpB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAY/C;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACtC,IAAI,GAAE,OAAqB,EAC3B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAwB7E;AA4BD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,OAAqB,EAC3B,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,OAAO,CAAC,IAAI,CAAC,CAkBf;AAqBD,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAU1E;AAED,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACpF,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAClE,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACjE,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAChC,GAAG,WAAW,CAoBd;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAAC;IAC7C,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,WAAW,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC9F,IAAI,GAAE,OAAqB,EAC3B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IACT,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChE,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B,CAAC,CAkCD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,GAAE,OAAqB,GAC1B,OAAO,CAAC;IACT,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnF,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChE,+EAA+E;IAC/E,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,qEAAqE;IACrE,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B,CAAC,CA6BD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACtC,IAAI,GAAE,OAAqB,EAC3B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAiB3D;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,GAAE,OAAqB,GAC1B,OAAO,CAAC;IACT,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACjE;;2EAEuE;IACvE,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC,CAAC,CAsBD"}
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 { [
|
|
24
|
+
return { [GCP_RESOURCE_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE };
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
/**
|
|
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
|
|
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<
|
|
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)
|
|
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)
|
|
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).
|
|
607
|
-
*
|
|
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.
|
|
753
|
+
const mapper = requireKind(r, args.manifestPath) ? MAPPERS[r.kind as string] : undefined;
|
|
626
754
|
if (!mapper) {
|
|
627
|
-
|
|
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
|
|
639
|
-
|
|
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<{
|
|
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.
|
|
827
|
+
const mapper = requireKind(r, args.manifestPath) ? MAPPERS[r.kind as string] : undefined;
|
|
689
828
|
if (!mapper) {
|
|
690
|
-
|
|
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
|
}
|
package/src/ownership.ts
ADDED
|
@@ -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
|
+
};
|