@intentius/chant-lexicon-gcp 0.124.38 → 0.124.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/api/read-client.d.ts +7 -0
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/api/read-client.js +24 -9
  4. package/dist/composites/catalog.d.ts.map +1 -1
  5. package/dist/composites/catalog.js +187 -0
  6. package/dist/composites/index.d.ts +4 -0
  7. package/dist/composites/index.d.ts.map +1 -1
  8. package/dist/composites/index.js +2 -0
  9. package/dist/composites/private-egress-network.d.ts +42 -0
  10. package/dist/composites/private-egress-network.d.ts.map +1 -0
  11. package/dist/composites/private-egress-network.js +48 -0
  12. package/dist/composites/public-cloud-run.d.ts +85 -0
  13. package/dist/composites/public-cloud-run.d.ts.map +1 -0
  14. package/dist/composites/public-cloud-run.js +144 -0
  15. package/dist/deep-observe.d.ts +5 -0
  16. package/dist/deep-observe.d.ts.map +1 -1
  17. package/dist/deep-observe.js +34 -2
  18. package/dist/health.d.ts +26 -2
  19. package/dist/health.d.ts.map +1 -1
  20. package/dist/health.js +85 -6
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/integrity.json +3 -3
  25. package/dist/manifest.json +1 -1
  26. package/dist/op/activities/gcp-apply.d.ts +19 -0
  27. package/dist/op/activities/gcp-apply.d.ts.map +1 -1
  28. package/dist/op/activities/gcp-apply.js +16 -0
  29. package/dist/op/activities/gcp-compute.d.ts +9 -0
  30. package/dist/op/activities/gcp-compute.d.ts.map +1 -1
  31. package/dist/op/activities/gcp-compute.js +107 -11
  32. package/dist/plugin.d.ts.map +1 -1
  33. package/dist/plugin.js +6 -2
  34. package/dist/skills/chant-gcp.md +2 -0
  35. package/package.json +2 -2
  36. package/src/api/read-client.ts +23 -8
  37. package/src/composites/catalog.ts +187 -0
  38. package/src/composites/cloud-run-lb.test.ts +93 -0
  39. package/src/composites/index.ts +4 -0
  40. package/src/composites/private-egress-network.ts +82 -0
  41. package/src/composites/public-cloud-run.ts +245 -0
  42. package/src/deep-observe.test.ts +46 -0
  43. package/src/deep-observe.ts +38 -2
  44. package/src/health.test.ts +65 -1
  45. package/src/health.ts +88 -11
  46. package/src/index.ts +2 -1
  47. package/src/op/activities/gcp-apply.ts +31 -1
  48. package/src/op/activities/gcp-compute-reconcile.test.ts +150 -0
  49. package/src/op/activities/gcp-compute.test.ts +4 -2
  50. package/src/op/activities/gcp-compute.ts +129 -11
  51. package/src/plugin.ts +6 -2
  52. package/src/skills/chant-gcp.md +2 -0
@@ -0,0 +1,150 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import { applyResource, MAPPERS, type GcpHttp, type GcpResource } from "./gcp-apply";
3
+ import { changedFields, computeLabels, holds } from "./gcp-compute";
4
+
5
+ beforeEach(() => {
6
+ delete process.env.GCP_REGION;
7
+ });
8
+
9
+ const base = "https://compute.googleapis.com";
10
+ const lb: GcpResource[] = [
11
+ {
12
+ kind: "ComputeForwardingRule",
13
+ metadata: { name: "web-fr" },
14
+ spec: {
15
+ location: "global",
16
+ portRange: "80",
17
+ ipAddress: { addressRef: { name: "web-ip" } },
18
+ target: { targetHTTPProxyRef: { name: "web-proxy" } },
19
+ },
20
+ },
21
+ { kind: "ComputeTargetHTTPProxy", metadata: { name: "web-proxy" }, spec: { location: "global", urlMapRef: { name: "web-map" } } },
22
+ { kind: "ComputeURLMap", metadata: { name: "web-map" }, spec: { location: "global", defaultService: { backendServiceRef: { name: "web" } } } },
23
+ {
24
+ kind: "ComputeBackendService",
25
+ metadata: { name: "web" },
26
+ spec: { location: "global", protocol: "HTTPS", timeoutSec: 30, backend: [{ group: { networkEndpointGroupRef: { name: "web" } } }] },
27
+ },
28
+ {
29
+ kind: "ComputeRegionNetworkEndpointGroup",
30
+ metadata: { name: "web" },
31
+ spec: { region: "europe-west1", networkEndpointType: "SERVERLESS", cloudRun: { serviceRef: { name: "web" } } },
32
+ },
33
+ { kind: "ComputeAddress", metadata: { name: "web-ip" }, spec: { location: "global" } },
34
+ ];
35
+ const ctx = { base, project: "p1", manifest: lb };
36
+ const byKind = (kind: string) => lb.find((r) => r.kind === kind)!;
37
+ const url = (path: string) => `${base}/compute/v1/projects/p1/${path}`;
38
+ const OP = JSON.stringify({ kind: "compute#operation", selfLink: url("global/operations/op-1"), status: "DONE" });
39
+
40
+ /** A fake compute API: GETs answer from `live`, every write is recorded and returns a finished operation. */
41
+ function fakeCompute(live: Record<string, unknown>) {
42
+ const writes: Array<{ method: string; url: string; body: unknown }> = [];
43
+ const http: GcpHttp = async (method, u, body) => {
44
+ if (method === "GET") {
45
+ const hit = live[u];
46
+ if (hit !== undefined) return { status: 200, text: JSON.stringify(hit) };
47
+ if (u.includes("/operations/")) return { status: 200, text: OP };
48
+ return { status: 404, text: "" };
49
+ }
50
+ writes.push({ method, url: u, body });
51
+ return { status: 200, text: OP };
52
+ };
53
+ return { http, writes };
54
+ }
55
+
56
+ describe("compute reconcile against the live resource (#4238)", () => {
57
+ test("holds: a full compute URL holds its partial form, and live-only fields don't count", () => {
58
+ expect(holds("projects/p1/global/urlMaps/m", url("global/urlMaps/m"))).toBe(true);
59
+ expect(holds({ a: 1 }, { a: 1, b: 2 })).toBe(true);
60
+ expect(holds([{ group: "projects/p1/x" }], [{ group: `${base}/compute/v1/projects/p1/x`, balancingMode: "UTILIZATION" }])).toBe(true);
61
+ expect(holds([1], [1, 2])).toBe(false);
62
+ expect(changedFields({ name: "n", a: 1, b: 2 }, { name: "other", a: 1, b: 3 })).toEqual(["b"]);
63
+ });
64
+
65
+ test("a backend service that matches is left alone; a change is a PATCH carrying the live fingerprint", async () => {
66
+ const liveBody = {
67
+ name: "web",
68
+ fingerprint: "fp-1",
69
+ protocol: "HTTPS",
70
+ timeoutSec: 30,
71
+ backends: [{ group: url("regions/europe-west1/networkEndpointGroups/web"), balancingMode: "UTILIZATION" }],
72
+ };
73
+ const same = fakeCompute({ [url("global/backendServices/web")]: liveBody });
74
+ await expect(applyResource(MAPPERS.ComputeBackendService, byKind("ComputeBackendService"), ctx, same.http)).resolves.toMatchObject({ updated: false });
75
+ expect(same.writes).toEqual([]);
76
+
77
+ const drifted = fakeCompute({ [url("global/backendServices/web")]: { ...liveBody, timeoutSec: 10 } });
78
+ await expect(applyResource(MAPPERS.ComputeBackendService, byKind("ComputeBackendService"), ctx, drifted.http)).resolves.toMatchObject({ updated: true });
79
+ expect(drifted.writes).toHaveLength(1);
80
+ expect(drifted.writes[0]).toMatchObject({ method: "PATCH", url: url("global/backendServices/web"), body: { timeoutSec: 30, fingerprint: "fp-1" } });
81
+ });
82
+
83
+ test("a URL map's new default service is a PATCH with the fingerprint", async () => {
84
+ const f = fakeCompute({ [url("global/urlMaps/web-map")]: { name: "web-map", fingerprint: "fp-m", defaultService: url("global/backendServices/old") } });
85
+ await applyResource(MAPPERS.ComputeURLMap, byKind("ComputeURLMap"), ctx, f.http);
86
+ expect(f.writes).toEqual([
87
+ { method: "PATCH", url: url("global/urlMaps/web-map"), body: expect.objectContaining({ defaultService: "projects/p1/global/backendServices/web", fingerprint: "fp-m" }) },
88
+ ]);
89
+ });
90
+
91
+ test("a proxy's URL map changes through setUrlMap", async () => {
92
+ const f = fakeCompute({ [url("global/targetHttpProxies/web-proxy")]: { name: "web-proxy", urlMap: url("global/urlMaps/old") } });
93
+ await applyResource(MAPPERS.ComputeTargetHTTPProxy, byKind("ComputeTargetHTTPProxy"), ctx, f.http);
94
+ expect(f.writes).toEqual([{ method: "POST", url: url("global/targetHttpProxies/web-proxy/setUrlMap"), body: { urlMap: "projects/p1/global/urlMaps/web-map" } }]);
95
+ });
96
+
97
+ test("a forwarding rule: the declared address's IP holds, the target changes by setTarget, labels by setLabels", async () => {
98
+ const f = fakeCompute({
99
+ [url("global/forwardingRules/web-fr")]: {
100
+ name: "web-fr",
101
+ portRange: "80",
102
+ IPAddress: "34.1.2.3",
103
+ target: url("global/targetHttpProxies/old"),
104
+ labels: {},
105
+ labelFingerprint: "lf-1",
106
+ },
107
+ [url("global/addresses/web-ip")]: { name: "web-ip", address: "34.1.2.3" },
108
+ });
109
+ await applyResource(MAPPERS.ComputeForwardingRule, byKind("ComputeForwardingRule"), ctx, f.http);
110
+ expect(f.writes).toEqual([
111
+ { method: "POST", url: url("global/forwardingRules/web-fr/setTarget"), body: { target: "projects/p1/global/targetHttpProxies/web-proxy" } },
112
+ { method: "POST", url: url("global/forwardingRules/web-fr/setLabels"), body: { labels: computeLabels(undefined), labelFingerprint: "lf-1" } },
113
+ ]);
114
+ });
115
+
116
+ test("a forwarding rule on another IP, a changed address and a changed NEG are reported for replacement, not written", async () => {
117
+ const fr = fakeCompute({
118
+ [url("global/forwardingRules/web-fr")]: {
119
+ name: "web-fr",
120
+ portRange: "80",
121
+ IPAddress: "10.0.0.9",
122
+ target: url("global/targetHttpProxies/web-proxy"),
123
+ labels: computeLabels(undefined),
124
+ },
125
+ [url("global/addresses/web-ip")]: { name: "web-ip", address: "34.1.2.3" },
126
+ });
127
+ const r = await applyResource(MAPPERS.ComputeForwardingRule, byKind("ComputeForwardingRule"), ctx, fr.http);
128
+ expect(r).toMatchObject({ updated: false, replaceRequired: expect.stringContaining("IPAddress") });
129
+ expect(fr.writes).toEqual([]);
130
+
131
+ const neg = fakeCompute({
132
+ [url("regions/europe-west1/networkEndpointGroups/web")]: { name: "web", networkEndpointType: "SERVERLESS", cloudRun: { service: "other" } },
133
+ });
134
+ const n = await applyResource(MAPPERS.ComputeRegionNetworkEndpointGroup, byKind("ComputeRegionNetworkEndpointGroup"), ctx, neg.http);
135
+ expect(n.replaceRequired).toMatch(/cloudRun/);
136
+ expect(neg.writes).toEqual([]);
137
+
138
+ const addr = fakeCompute({ [url("global/addresses/web-ip")]: { name: "web-ip", address: "34.1.2.3", labels: computeLabels(undefined), addressType: "INTERNAL" } });
139
+ const changed = { ...byKind("ComputeAddress"), spec: { location: "global", addressType: "EXTERNAL" } };
140
+ const a = await applyResource(MAPPERS.ComputeAddress, changed, ctx, addr.http);
141
+ expect(a.replaceRequired).toMatch(/addressType/);
142
+ expect(addr.writes).toEqual([]);
143
+ });
144
+
145
+ test("an address's labels change through setLabels", async () => {
146
+ const f = fakeCompute({ [url("global/addresses/web-ip")]: { name: "web-ip", address: "34.1.2.3", labels: {}, labelFingerprint: "lf-a" } });
147
+ await applyResource(MAPPERS.ComputeAddress, byKind("ComputeAddress"), ctx, f.http);
148
+ expect(f.writes).toEqual([{ method: "POST", url: url("global/addresses/web-ip/setLabels"), body: { labels: computeLabels(undefined), labelFingerprint: "lf-a" } }]);
149
+ });
150
+ });
@@ -60,7 +60,7 @@ describe("compute load balancer mappers (#4233)", () => {
60
60
  });
61
61
 
62
62
  test("a serverless NEG is regional and names its Cloud Run service", () => {
63
- expect(plan("ComputeRegionNetworkEndpointGroup")).toEqual({
63
+ expect(plan("ComputeRegionNetworkEndpointGroup")).toMatchObject({
64
64
  getUrl: "https://compute.googleapis.com/compute/v1/projects/p1/regions/europe-west1/networkEndpointGroups/web",
65
65
  create: {
66
66
  method: "POST",
@@ -138,7 +138,9 @@ describe("compute operations (#4233)", () => {
138
138
  let exists = false;
139
139
  const http: GcpHttp = async (method, url) => {
140
140
  calls.push(`${method} ${url}`);
141
- if (method === "GET" && url.endsWith("/addresses/web-ip")) return exists ? { status: 200, text: "{}" } : { status: 404, text: "" };
141
+ if (method === "GET" && url.endsWith("/addresses/web-ip")) {
142
+ return exists ? { status: 200, text: JSON.stringify({ name: "web-ip", labels: computeLabels(undefined) }) } : { status: 404, text: "" };
143
+ }
142
144
  if (method === "POST") {
143
145
  exists = true;
144
146
  return { status: 200, text: JSON.stringify({ kind: "compute#operation", selfLink: "https://compute.googleapis.com/compute/v1/projects/p1/global/operations/op-1", status: "PENDING" }) };
@@ -14,13 +14,20 @@
14
14
  * backend service can point at a regional NEG.
15
15
  *
16
16
  * Inserts return a compute `Operation`, polled at its `selfLink` (rebased onto
17
- * the endpoint override, so floci-gcp answers). These kinds reconcile by
18
- * existence only: an existing resource is left as it is, since compute updates
19
- * need the live `fingerprint` and most of these fields are immutable.
17
+ * the endpoint override, so floci-gcp answers).
18
+ *
19
+ * An existing load balancer resource is compared with its desired body and
20
+ * updated in place where compute allows it (#4238): a backend service or URL
21
+ * map by PATCH with the live `fingerprint`, a proxy's URL map by `setUrlMap`,
22
+ * a forwarding rule's target by `setTarget`, labels by `setLabels` with the
23
+ * live `labelFingerprint`. A difference on a field compute can't change in
24
+ * place (anything on a serverless NEG, an address's IP) is reported as
25
+ * "replace required" and left alone. A network and a subnetwork still
26
+ * reconcile by existence only.
20
27
  */
21
28
  import { GCP_RESOURCE_OWNERSHIP_KEYS } from "../../ownership";
22
29
  import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
23
- import type { GcpResource, OperationSpec, ResourceMapper } from "./gcp-apply";
30
+ import type { GcpHttp, GcpResource, OperationSpec, ReconcileStep, ResourceMapper } from "./gcp-apply";
24
31
 
25
32
  export const COMPUTE_HOST = "https://compute.googleapis.com";
26
33
 
@@ -324,12 +331,103 @@ export const computeOperation: OperationSpec = {
324
331
  },
325
332
  };
326
333
 
334
+ /** A compute reference with everything before `projects/` dropped, so a full URL and a partial one compare equal. */
335
+ function refTail(v: string): string {
336
+ const at = v.indexOf("projects/");
337
+ return at >= 0 ? v.slice(at) : v;
338
+ }
339
+
340
+ /**
341
+ * True when the live value holds the desired one: every desired key is set
342
+ * live to an equal value, arrays element by element, and a compute URL
343
+ * equals its partial form. Fields only the live side has (defaults, server
344
+ * ids) don't count. Pure.
345
+ */
346
+ export function holds(desired: unknown, live: unknown): boolean {
347
+ if (typeof desired === "string" && typeof live === "string") {
348
+ return desired === live || (desired.includes("projects/") && refTail(desired) === refTail(live));
349
+ }
350
+ if (Array.isArray(desired)) {
351
+ return Array.isArray(live) && desired.length === live.length && desired.every((d, i) => holds(d, live[i]));
352
+ }
353
+ if (isObj(desired)) {
354
+ return isObj(live) && Object.entries(desired).every(([k, v]) => holds(v, live[k]));
355
+ }
356
+ return desired === live;
357
+ }
358
+
359
+ /** The desired body's top-level fields the live resource doesn't hold, `name` aside. Pure. */
360
+ export function changedFields(desired: Obj, live: Obj): string[] {
361
+ return Object.keys(desired).filter((k) => k !== "name" && !holds(desired[k], live[k]));
362
+ }
363
+
364
+ type Reconcile = (args: {
365
+ url: string;
366
+ desired: Obj;
367
+ live: Obj;
368
+ changed: string[];
369
+ ctx: ComputeCtx;
370
+ http: GcpHttp;
371
+ signal?: AbortSignal;
372
+ }) => Promise<ReconcileStep>;
373
+
374
+ const replace = (kind: string, fields: readonly string[]): ReconcileStep => ({
375
+ replace: `${fields.join(", ")} differ${fields.length === 1 ? "s" : ""}, and compute can't change ${fields.length === 1 ? "it" : "them"} on a ${kind} in place`,
376
+ });
377
+
378
+ /** `labels` by `setLabels` with the live `labelFingerprint`; anything else changed is a replace. */
379
+ function labelsOnly(kind: string): Reconcile {
380
+ return async ({ url, desired, live, changed }) => {
381
+ const other = changed.filter((f) => f !== "labels");
382
+ if (other.length) return replace(kind, other);
383
+ if (!changed.length) return { requests: [] };
384
+ return { requests: [{ method: "POST", url: `${url}/setLabels`, body: { labels: desired.labels, labelFingerprint: live.labelFingerprint } }] };
385
+ };
386
+ }
387
+
388
+ /** Every changed field by one PATCH carrying the live `fingerprint` (backend services, URL maps). */
389
+ const patchWithFingerprint: Reconcile = async ({ url, desired, live, changed }) =>
390
+ changed.length ? { requests: [{ method: "PATCH", url, body: { ...desired, fingerprint: live.fingerprint } }] } : { requests: [] };
391
+
392
+ const reconcileProxy: Reconcile = async ({ url, desired, changed }) => {
393
+ const other = changed.filter((f) => f !== "urlMap");
394
+ if (other.length) return replace("ComputeTargetHTTPProxy", other);
395
+ return changed.length ? { requests: [{ method: "POST", url: `${url}/setUrlMap`, body: { urlMap: desired.urlMap } }] } : { requests: [] };
396
+ };
397
+
398
+ /**
399
+ * A forwarding rule's `IPAddress` is a declared address's partial URL, where
400
+ * the live rule holds the IP itself, so the address is read and its IP
401
+ * compared. The target changes by `setTarget`, labels by `setLabels`; any
402
+ * other field is a replace.
403
+ */
404
+ const reconcileForwardingRule: Reconcile = async ({ url, desired, live, changed, ctx, http, signal }) => {
405
+ let fields = changed;
406
+ const ip = desired.IPAddress;
407
+ if (fields.includes("IPAddress") && typeof ip === "string" && ip.includes("/addresses/")) {
408
+ const res = await http("GET", `${ctx.base}/compute/v1/${refTail(ip)}`, undefined, signal);
409
+ const address = res.status === 200 ? (JSON.parse(res.text) as Obj).address : undefined;
410
+ if (address !== undefined && address === live.IPAddress) fields = fields.filter((f) => f !== "IPAddress");
411
+ }
412
+ const other = fields.filter((f) => f !== "target" && f !== "labels");
413
+ if (other.length) return replace("ComputeForwardingRule", other);
414
+ const requests: Array<{ method: "POST"; url: string; body: unknown }> = [];
415
+ if (fields.includes("target")) requests.push({ method: "POST", url: `${url}/setTarget`, body: { target: desired.target } });
416
+ if (fields.includes("labels")) {
417
+ requests.push({ method: "POST", url: `${url}/setLabels`, body: { labels: desired.labels, labelFingerprint: live.labelFingerprint } });
418
+ }
419
+ return { requests };
420
+ };
421
+
422
+ const replaceOnAnyChange = (kind: string): Reconcile => async ({ changed }) => (changed.length ? replace(kind, changed) : { requests: [] });
423
+
327
424
  function computeMapper(
328
425
  kind: string,
329
426
  collection: string,
330
427
  build: (resource: GcpResource, ctx: ComputeCtx) => Obj,
331
428
  regionalCollection = collection,
332
429
  scopeFor: (spec: Obj | undefined) => string = (spec) => scopeOf(spec),
430
+ reconcile?: Reconcile,
333
431
  ): ResourceMapper {
334
432
  return {
335
433
  kind,
@@ -339,16 +437,36 @@ function computeMapper(
339
437
  const scope = scopeFor(resource.spec as Obj | undefined);
340
438
  const coll = `${ctx.base}/compute/v1/${scopePath(ctx.project, scope)}/${scope === "global" ? collection : regionalCollection}`;
341
439
  const name = computeName(resource);
342
- return { getUrl: `${coll}/${enc(name)}`, create: { method: "POST", url: coll, body: build(resource, ctx) } };
440
+ const url = `${coll}/${enc(name)}`;
441
+ const desired = build(resource, ctx);
442
+ return {
443
+ getUrl: url,
444
+ create: { method: "POST", url: coll, body: desired },
445
+ ...(reconcile
446
+ ? {
447
+ reconcile: (live: unknown, http: GcpHttp, signal?: AbortSignal) => {
448
+ const l = isObj(live) ? live : {};
449
+ return reconcile({ url, desired, live: l, changed: changedFields(desired, l), ctx, http, signal });
450
+ },
451
+ }
452
+ : {}),
453
+ };
343
454
  },
344
455
  };
345
456
  }
346
457
 
347
- export const computeAddressMapper = computeMapper("ComputeAddress", "addresses", addressBody);
348
- export const regionNetworkEndpointGroupMapper = computeMapper("ComputeRegionNetworkEndpointGroup", "networkEndpointGroups", negBody);
349
- export const backendServiceMapper = computeMapper("ComputeBackendService", "backendServices", backendServiceBody);
350
- export const urlMapMapper = computeMapper("ComputeURLMap", "urlMaps", urlMapBody);
351
- export const targetHttpProxyMapper = computeMapper("ComputeTargetHTTPProxy", "targetHttpProxies", targetHttpProxyBody);
352
- export const forwardingRuleMapper = computeMapper("ComputeForwardingRule", "forwardingRules", forwardingRuleBody);
458
+ export const computeAddressMapper = computeMapper("ComputeAddress", "addresses", addressBody, undefined, undefined, labelsOnly("ComputeAddress"));
459
+ export const regionNetworkEndpointGroupMapper = computeMapper(
460
+ "ComputeRegionNetworkEndpointGroup",
461
+ "networkEndpointGroups",
462
+ negBody,
463
+ undefined,
464
+ undefined,
465
+ replaceOnAnyChange("ComputeRegionNetworkEndpointGroup"),
466
+ );
467
+ export const backendServiceMapper = computeMapper("ComputeBackendService", "backendServices", backendServiceBody, undefined, undefined, patchWithFingerprint);
468
+ export const urlMapMapper = computeMapper("ComputeURLMap", "urlMaps", urlMapBody, undefined, undefined, patchWithFingerprint);
469
+ export const targetHttpProxyMapper = computeMapper("ComputeTargetHTTPProxy", "targetHttpProxies", targetHttpProxyBody, undefined, undefined, reconcileProxy);
470
+ export const forwardingRuleMapper = computeMapper("ComputeForwardingRule", "forwardingRules", forwardingRuleBody, undefined, undefined, reconcileForwardingRule);
353
471
  export const networkMapper = computeMapper("ComputeNetwork", "networks", networkBody, "networks", () => "global");
354
472
  export const subnetworkMapper = computeMapper("ComputeSubnetwork", "subnetworks", subnetworkBody);
package/src/plugin.ts CHANGED
@@ -411,8 +411,12 @@ export const bucket = new StorageBucket({
411
411
 
412
412
  deepNormalizationHooks: gcpDeepNormalizationHooks,
413
413
 
414
- /** Runtime health (#4177): Cloud Run services. Implementation in ./health.ts. */
415
- healthTypes: ["GCP::Run::Service"],
414
+ /**
415
+ * Runtime health (#4177, #4239): Cloud Run services, backend services and IAM
416
+ * policy members. Implementation in ./health.ts, whose GCP_HEALTH_TYPES this
417
+ * list matches; written out so the build never imports the reader.
418
+ */
419
+ healthTypes: ["GCP::Run::Service", "GCP::Compute::BackendService", "GCP::Iam::PolicyMember"],
416
420
 
417
421
  async observeHealth(options) {
418
422
  const { observeHealthGcp } = await import("./health");
@@ -202,6 +202,8 @@ Composites group related Config Connector resources with secure defaults. Each r
202
202
  | `ManagedCertificate` | ComputeManagedSslCertificate | Auto-renewal via Google-managed cert |
203
203
  | `SecureProject` | Project + IAMAuditConfig + essential Service resources | Audit logging, required APIs enabled, org policy constraints |
204
204
  | `GpuNodePool` | ContainerNodePool with GPU accelerators | Scale-to-zero, nvidia.com/gpu taint, GKE metadata workload identity |
205
+ | `PrivateEgressNetwork` | ComputeNetwork + ComputeSubnetwork | Custom mode, regional routing, Private Google Access |
206
+ | `PublicCloudRun` | IAMServiceAccount + RunService + serverless NEG + global external Application Load Balancer | Own service account, Direct VPC egress for private ranges, optional managed certificate with HTTP to HTTPS redirect, optional Cloud Armor policy |
205
207
 
206
208
  ### Example: GKE cluster with secure defaults
207
209