@intentius/chant-lexicon-gcp 0.124.34 → 0.124.36
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/api/read-client.d.ts +8 -3
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/api/read-client.js +37 -15
- package/dist/composites/cloud-run-service.d.ts +2 -0
- package/dist/composites/cloud-run-service.d.ts.map +1 -1
- package/dist/composites/cloud-run-service.js +4 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +176 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/gcp-apply.d.ts +62 -18
- package/dist/op/activities/gcp-apply.d.ts.map +1 -1
- package/dist/op/activities/gcp-apply.js +128 -16
- package/dist/op/activities/gcp-compute.d.ts +66 -0
- package/dist/op/activities/gcp-compute.d.ts.map +1 -0
- package/dist/op/activities/gcp-compute.js +349 -0
- package/dist/op/activities/gcp-iam.d.ts +82 -0
- package/dist/op/activities/gcp-iam.d.ts.map +1 -0
- package/dist/op/activities/gcp-iam.js +215 -0
- package/dist/serializer.d.ts +6 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +21 -9
- package/package.json +2 -2
- package/src/api/read-client.ts +36 -16
- package/src/composites/cloud-run-service.ts +6 -0
- package/src/composites/composites.test.ts +7 -0
- package/src/deep-observe.test.ts +42 -1
- package/src/deep-observe.ts +171 -0
- package/src/describe-resources.test.ts +1 -1
- package/src/op/activities/gcp-apply.test.ts +75 -0
- package/src/op/activities/gcp-apply.ts +167 -22
- package/src/op/activities/gcp-compute.test.ts +306 -0
- package/src/op/activities/gcp-compute.ts +354 -0
- package/src/op/activities/gcp-iam.test.ts +152 -0
- package/src/op/activities/gcp-iam.ts +284 -0
- package/src/serializer.test.ts +26 -0
- package/src/serializer.ts +22 -9
|
@@ -3,6 +3,8 @@ import { parseYAML } from "@intentius/chant/yaml";
|
|
|
3
3
|
import { sleep } from "@intentius/chant/op";
|
|
4
4
|
import { hasOwnershipMarker, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
5
5
|
import { GCP_RESOURCE_OWNERSHIP_KEYS } from "../../ownership.js";
|
|
6
|
+
import { iamPolicyMemberMapper } from "./gcp-iam.js";
|
|
7
|
+
import { backendServiceMapper, computeAddressMapper, forwardingRuleMapper, networkMapper, subnetworkMapper, regionNetworkEndpointGroupMapper, targetHttpProxyMapper, urlMapMapper, } from "./gcp-compute.js";
|
|
6
8
|
import { applyResult, } from "@intentius/chant/apply";
|
|
7
9
|
const PROJECT_ID_ANNOTATION = "cnrm.cloud.google.com/project-id";
|
|
8
10
|
/**
|
|
@@ -143,14 +145,71 @@ export const pubSubTopicMapper = {
|
|
|
143
145
|
items: (body) => (body?.topics ?? []).map((t) => ({ name: t.name.split("/").pop() ?? t.name, labels: t.labels })),
|
|
144
146
|
},
|
|
145
147
|
};
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Map a CNRM RunService to a Cloud Run v2 Service body. Pure. CNRM names the
|
|
150
|
+
* service account with `template.serviceAccountRef`; v2 takes
|
|
151
|
+
* `template.serviceAccount` as an email (#4240).
|
|
152
|
+
*/
|
|
153
|
+
export function cloudRunServiceBody(resource, project) {
|
|
148
154
|
const spec = (resource.spec ?? {});
|
|
149
155
|
const body = {};
|
|
150
|
-
if (spec.template)
|
|
151
|
-
body
|
|
156
|
+
if (!spec.template)
|
|
157
|
+
return body;
|
|
158
|
+
const { serviceAccountRef, ...template } = spec.template;
|
|
159
|
+
const email = serviceAccountEmail(serviceAccountRef, project);
|
|
160
|
+
const out = email ? { ...template, serviceAccount: email } : { ...template };
|
|
161
|
+
const vpcAccess = out.vpcAccess;
|
|
162
|
+
if (vpcAccess && Array.isArray(vpcAccess.networkInterfaces)) {
|
|
163
|
+
const region = (resource.spec ?? {}).location ?? "us-central1";
|
|
164
|
+
out.vpcAccess = { ...vpcAccess, networkInterfaces: vpcAccess.networkInterfaces.map((i) => networkInterface(i, project, region)) };
|
|
165
|
+
}
|
|
166
|
+
body.template = out;
|
|
152
167
|
return body;
|
|
153
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* A CNRM Direct VPC egress interface as Cloud Run v2 takes it (#4249):
|
|
171
|
+
* `networkRef` and `subnetworkRef` become `network` and `subnetwork` paths,
|
|
172
|
+
* `name` in the project (the subnetwork in the service's region) and
|
|
173
|
+
* `external` as given. `tags` and anything else pass through. Pure.
|
|
174
|
+
*/
|
|
175
|
+
export function networkInterface(iface, project, region) {
|
|
176
|
+
if (!iface || typeof iface !== "object")
|
|
177
|
+
return iface;
|
|
178
|
+
const { networkRef, subnetworkRef, ...rest } = iface;
|
|
179
|
+
const path = (ref, scoped, key) => {
|
|
180
|
+
if (!ref)
|
|
181
|
+
return undefined;
|
|
182
|
+
if (ref.external)
|
|
183
|
+
return ref.external;
|
|
184
|
+
if (!ref.name)
|
|
185
|
+
return undefined;
|
|
186
|
+
if (!project)
|
|
187
|
+
throw new Error(`${key} { name: "${ref.name}" } needs a project to form its path`);
|
|
188
|
+
return scoped(project, ref.name);
|
|
189
|
+
};
|
|
190
|
+
const network = path(networkRef, (p, n) => `projects/${p}/global/networks/${n}`, "networkRef");
|
|
191
|
+
const subnetwork = path(subnetworkRef, (p, n) => `projects/${p}/regions/${region}/subnetworks/${n}`, "subnetworkRef");
|
|
192
|
+
return { ...rest, ...(network ? { network } : {}), ...(subnetwork ? { subnetwork } : {}) };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The email a CNRM `serviceAccountRef` names: `external` as given (an email,
|
|
196
|
+
* or a `projects/<p>/serviceAccounts/<email>` path), `name` as an account id
|
|
197
|
+
* in the project.
|
|
198
|
+
*/
|
|
199
|
+
export function serviceAccountEmail(ref, project) {
|
|
200
|
+
if (!ref)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (ref.external)
|
|
203
|
+
return ref.external.split("/").pop();
|
|
204
|
+
if (ref.name) {
|
|
205
|
+
if (ref.name.includes("@"))
|
|
206
|
+
return ref.name;
|
|
207
|
+
if (!project)
|
|
208
|
+
throw new Error(`serviceAccountRef { name: "${ref.name}" } needs a project to form the account's email`);
|
|
209
|
+
return `${ref.name}@${project}.iam.gserviceaccount.com`;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
154
213
|
export const cloudRunServiceMapper = {
|
|
155
214
|
kind: "RunService",
|
|
156
215
|
defaultHost: "https://run.googleapis.com",
|
|
@@ -168,11 +227,11 @@ export const cloudRunServiceMapper = {
|
|
|
168
227
|
create: {
|
|
169
228
|
method: "POST",
|
|
170
229
|
url: `${services}?serviceId=${encodeURIComponent(name)}`,
|
|
171
|
-
body: cloudRunServiceBody(resource),
|
|
230
|
+
body: cloudRunServiceBody(resource, project),
|
|
172
231
|
},
|
|
173
232
|
// Cloud Run reconciles with a PATCH of the service; like create it returns
|
|
174
233
|
// a long-running operation, polled via the mapper's `operation`.
|
|
175
|
-
update: { method: "PATCH", url, body: cloudRunServiceBody(resource) },
|
|
234
|
+
update: { method: "PATCH", url, body: cloudRunServiceBody(resource, project) },
|
|
176
235
|
};
|
|
177
236
|
},
|
|
178
237
|
};
|
|
@@ -259,6 +318,16 @@ export const MAPPERS = {
|
|
|
259
318
|
SecretManagerSecret: secretManagerSecretMapper,
|
|
260
319
|
IAMServiceAccount: gcpServiceAccountMapper,
|
|
261
320
|
RunService: cloudRunServiceMapper,
|
|
321
|
+
// #4233: a binding member, and the Cloud Run load balancer.
|
|
322
|
+
IAMPolicyMember: iamPolicyMemberMapper,
|
|
323
|
+
ComputeAddress: computeAddressMapper,
|
|
324
|
+
ComputeRegionNetworkEndpointGroup: regionNetworkEndpointGroupMapper,
|
|
325
|
+
ComputeBackendService: backendServiceMapper,
|
|
326
|
+
ComputeURLMap: urlMapMapper,
|
|
327
|
+
ComputeTargetHTTPProxy: targetHttpProxyMapper,
|
|
328
|
+
ComputeForwardingRule: forwardingRuleMapper,
|
|
329
|
+
ComputeNetwork: networkMapper,
|
|
330
|
+
ComputeSubnetwork: subnetworkMapper,
|
|
262
331
|
};
|
|
263
332
|
/**
|
|
264
333
|
* Collect the local resource names this resource references via CNRM `*Ref`
|
|
@@ -266,7 +335,29 @@ export const MAPPERS = {
|
|
|
266
335
|
* refs point outside the manifest and are ignored for ordering. Pure.
|
|
267
336
|
*/
|
|
268
337
|
export function referencedNames(resource) {
|
|
269
|
-
|
|
338
|
+
return [...new Set(referencedResources(resource).map((r) => r.name))];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The CNRM kind a `*Ref` key points at, where the key alone says. A ref that
|
|
342
|
+
* carries its own `kind` (`resourceRef`) uses that. Two resources of different
|
|
343
|
+
* kinds may share a name (a RunService `web` and its NEG `web`), so ordering
|
|
344
|
+
* matches on kind when it can.
|
|
345
|
+
*/
|
|
346
|
+
const REF_KINDS = {
|
|
347
|
+
topicRef: "PubSubTopic",
|
|
348
|
+
serviceRef: "RunService",
|
|
349
|
+
serviceAccountRef: "IAMServiceAccount",
|
|
350
|
+
addressRef: "ComputeAddress",
|
|
351
|
+
networkEndpointGroupRef: "ComputeRegionNetworkEndpointGroup",
|
|
352
|
+
backendServiceRef: "ComputeBackendService",
|
|
353
|
+
urlMapRef: "ComputeURLMap",
|
|
354
|
+
targetHTTPProxyRef: "ComputeTargetHTTPProxy",
|
|
355
|
+
networkRef: "ComputeNetwork",
|
|
356
|
+
subnetworkRef: "ComputeSubnetwork",
|
|
357
|
+
};
|
|
358
|
+
/** The local resources this resource references via `*Ref` fields, each with its kind when known. Pure. */
|
|
359
|
+
export function referencedResources(resource) {
|
|
360
|
+
const refs = [];
|
|
270
361
|
const visit = (v, key) => {
|
|
271
362
|
if (Array.isArray(v)) {
|
|
272
363
|
for (const x of v)
|
|
@@ -276,14 +367,15 @@ export function referencedNames(resource) {
|
|
|
276
367
|
if (v && typeof v === "object") {
|
|
277
368
|
const obj = v;
|
|
278
369
|
if (key && /Refs?$/.test(key) && typeof obj.name === "string") {
|
|
279
|
-
|
|
370
|
+
const kind = typeof obj.kind === "string" ? obj.kind : REF_KINDS[key];
|
|
371
|
+
refs.push(kind ? { name: obj.name, kind } : { name: obj.name });
|
|
280
372
|
}
|
|
281
373
|
for (const [k, val] of Object.entries(obj))
|
|
282
374
|
visit(val, k);
|
|
283
375
|
}
|
|
284
376
|
};
|
|
285
377
|
visit(resource.spec);
|
|
286
|
-
return
|
|
378
|
+
return refs;
|
|
287
379
|
}
|
|
288
380
|
/**
|
|
289
381
|
* Topologically order resources so a referenced resource comes before the
|
|
@@ -296,7 +388,7 @@ export function orderByReferences(resources) {
|
|
|
296
388
|
for (const r of resources) {
|
|
297
389
|
const n = r.metadata?.name;
|
|
298
390
|
if (n)
|
|
299
|
-
byName.set(n, r);
|
|
391
|
+
byName.set(n, [...(byName.get(n) ?? []), r]);
|
|
300
392
|
}
|
|
301
393
|
const ordered = [];
|
|
302
394
|
const done = new Set();
|
|
@@ -307,9 +399,10 @@ export function orderByReferences(resources) {
|
|
|
307
399
|
if (active.has(r))
|
|
308
400
|
throw new Error(`reference cycle involving ${r.metadata?.name ?? "?"}`);
|
|
309
401
|
active.add(r);
|
|
310
|
-
for (const ref of
|
|
311
|
-
const
|
|
312
|
-
|
|
402
|
+
for (const ref of referencedResources(r)) {
|
|
403
|
+
const named = (byName.get(ref.name) ?? []).filter((d) => d !== r);
|
|
404
|
+
const deps = ref.kind ? named.filter((d) => d.kind === ref.kind) : named;
|
|
405
|
+
for (const dep of deps)
|
|
313
406
|
visit(dep);
|
|
314
407
|
}
|
|
315
408
|
active.delete(r);
|
|
@@ -379,8 +472,12 @@ const defaultHttp = async (method, url, body, signal) => {
|
|
|
379
472
|
* completion. `http` is injectable for tests.
|
|
380
473
|
*/
|
|
381
474
|
export async function applyResource(mapper, resource, ctx, http = defaultHttp, signal) {
|
|
382
|
-
const plan = mapper.plan(resource, ctx);
|
|
383
475
|
const name = resource.metadata?.name ?? "?";
|
|
476
|
+
if (mapper.custom) {
|
|
477
|
+
const result = await mapper.custom.apply(resource, ctx, http, signal);
|
|
478
|
+
return { kind: mapper.kind, name, ...result };
|
|
479
|
+
}
|
|
480
|
+
const plan = mapper.plan(resource, ctx);
|
|
384
481
|
const get = await http("GET", plan.getUrl, undefined, signal);
|
|
385
482
|
if (get.status === 200) {
|
|
386
483
|
// Exists. Reconcile to desired if the mapper supports it; otherwise leave it.
|
|
@@ -566,6 +663,7 @@ export async function gcpApply(args, signal, http = defaultHttp) {
|
|
|
566
663
|
const resolve = (mapper, resource) => ({
|
|
567
664
|
base: (endpoint ?? mapper.defaultHost).replace(/\/$/, ""),
|
|
568
665
|
project: args.project ?? (resource ? resolveGcpProject(resource) : ""),
|
|
666
|
+
manifest: resources,
|
|
569
667
|
});
|
|
570
668
|
const applied = [];
|
|
571
669
|
const notAttempted = [];
|
|
@@ -578,6 +676,11 @@ export async function gcpApply(args, signal, http = defaultHttp) {
|
|
|
578
676
|
continue;
|
|
579
677
|
}
|
|
580
678
|
const ctx = resolve(mapper, r);
|
|
679
|
+
if (mapper.accepts && !mapper.accepts(r, ctx)) {
|
|
680
|
+
console.log(`skip: ${r.kind}/${r.metadata?.name ?? "?"} has nothing chant can apply it to`);
|
|
681
|
+
notAttempted.push({ kind: r.kind, name: r.metadata?.name ?? "?", reason: "unsupported-kind" });
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
581
684
|
const result = await applyResource(mapper, r, ctx, http, signal);
|
|
582
685
|
const verb = result.created ? "created" : result.updated ? "updated" : "unchanged";
|
|
583
686
|
console.log(`${verb}: ${result.kind}/${result.name} (${ctx.base})`);
|
|
@@ -594,8 +697,11 @@ export async function gcpApply(args, signal, http = defaultHttp) {
|
|
|
594
697
|
* that is polled to completion. `http` is injectable for tests.
|
|
595
698
|
*/
|
|
596
699
|
export async function deleteResource(mapper, resource, ctx, http = defaultHttp, signal) {
|
|
597
|
-
const plan = mapper.plan(resource, ctx);
|
|
598
700
|
const name = resource.metadata?.name ?? "?";
|
|
701
|
+
if (mapper.custom) {
|
|
702
|
+
return { kind: mapper.kind, name, deleted: await mapper.custom.delete(resource, ctx, http, signal) };
|
|
703
|
+
}
|
|
704
|
+
const plan = mapper.plan(resource, ctx);
|
|
599
705
|
const res = await http("DELETE", plan.getUrl, undefined, signal);
|
|
600
706
|
if (res.status === 404) {
|
|
601
707
|
return { kind: mapper.kind, name, deleted: false };
|
|
@@ -633,7 +739,13 @@ export async function gcpDelete(args, signal, http = defaultHttp) {
|
|
|
633
739
|
}
|
|
634
740
|
const base = (resolveGcpEndpoint(args) ?? mapper.defaultHost).replace(/\/$/, "");
|
|
635
741
|
const project = args.project ?? resolveGcpProject(r);
|
|
636
|
-
const
|
|
742
|
+
const ctx = { base, project, manifest: resources };
|
|
743
|
+
if (mapper.accepts && !mapper.accepts(r, ctx)) {
|
|
744
|
+
console.log(`skip: ${r.kind}/${r.metadata?.name ?? "?"} has nothing chant can delete it from`);
|
|
745
|
+
notAttempted.push({ kind: r.kind, name: r.metadata?.name ?? "?", reason: "unsupported-kind" });
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const result = await deleteResource(mapper, r, ctx, http, signal);
|
|
637
749
|
console.log(`${result.deleted ? "deleted" : "absent"}: ${result.kind}/${result.name} (${base})`);
|
|
638
750
|
deleted.push(result);
|
|
639
751
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GcpResource, OperationSpec, ResourceMapper } from "./gcp-apply.js";
|
|
2
|
+
export declare const COMPUTE_HOST = "https://compute.googleapis.com";
|
|
3
|
+
type Ref = {
|
|
4
|
+
name?: string;
|
|
5
|
+
external?: string;
|
|
6
|
+
kind?: string;
|
|
7
|
+
};
|
|
8
|
+
type Obj = Record<string, unknown>;
|
|
9
|
+
export interface ComputeCtx {
|
|
10
|
+
base: string;
|
|
11
|
+
project: string;
|
|
12
|
+
manifest?: readonly GcpResource[];
|
|
13
|
+
}
|
|
14
|
+
/** The region or `"global"` a CNRM compute spec declares. A NEG names `region`; the rest `location`. */
|
|
15
|
+
export declare function scopeOf(spec: Obj | undefined, env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
/** The compute name: CNRM's `resourceID` when set, else `metadata.name`. */
|
|
17
|
+
export declare function computeName(resource: GcpResource): string;
|
|
18
|
+
/**
|
|
19
|
+
* The partial URL a `*Ref` resolves to. `external` is used as given when it is
|
|
20
|
+
* a path or URL; a bare name (or `name`) is scoped by the referenced resource's
|
|
21
|
+
* own spec when the manifest holds it, else by `fallbackScope`. Pure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refUrl(key: string, ref: Ref, ctx: ComputeCtx, fallbackScope: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The resource's labels GCP accepts, plus the ownership marker. Composites put
|
|
26
|
+
* Kubernetes-style keys (`app.kubernetes.io/component`) in `metadata.labels`,
|
|
27
|
+
* which a compute label key can't hold, so those are left out.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeLabels(labels: Record<string, string> | undefined): Record<string, string>;
|
|
30
|
+
/** ComputeAddress → addresses.insert body. Pure. */
|
|
31
|
+
export declare function addressBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
32
|
+
/** ComputeRegionNetworkEndpointGroup → regionNetworkEndpointGroups.insert body (serverless NEGs). Pure. */
|
|
33
|
+
export declare function negBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
34
|
+
/**
|
|
35
|
+
* ComputeNetwork → networks.insert body. A network is global. CNRM's
|
|
36
|
+
* `autoCreateSubnetworks` defaults to false, where compute's default is true,
|
|
37
|
+
* so it is always sent. Pure.
|
|
38
|
+
*/
|
|
39
|
+
export declare function networkBody(resource: GcpResource): Obj;
|
|
40
|
+
/** ComputeSubnetwork → subnetworks.insert body. CNRM's `secondaryIpRange` and `logConfig` become compute's `secondaryIpRanges` and an enabled `logConfig`. Pure. */
|
|
41
|
+
export declare function subnetworkBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
42
|
+
/** ComputeBackendService → backendServices.insert body. Pure. */
|
|
43
|
+
export declare function backendServiceBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
44
|
+
/** ComputeURLMap → urlMaps.insert body. Pure. */
|
|
45
|
+
export declare function urlMapBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
46
|
+
/** ComputeTargetHTTPProxy → targetHttpProxies.insert body. Pure. */
|
|
47
|
+
export declare function targetHttpProxyBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
48
|
+
/** ComputeForwardingRule → forwardingRules.insert body. Pure. */
|
|
49
|
+
export declare function forwardingRuleBody(resource: GcpResource, ctx: ComputeCtx): Obj;
|
|
50
|
+
/**
|
|
51
|
+
* A compute `Operation`: the insert or delete returns one with a `selfLink`,
|
|
52
|
+
* polled until `status: "DONE"`. The link is rebased onto `base`, so an
|
|
53
|
+
* endpoint override (floci-gcp) is polled too. A create that returned the
|
|
54
|
+
* resource itself has nothing to poll.
|
|
55
|
+
*/
|
|
56
|
+
export declare const computeOperation: OperationSpec;
|
|
57
|
+
export declare const computeAddressMapper: ResourceMapper;
|
|
58
|
+
export declare const regionNetworkEndpointGroupMapper: ResourceMapper;
|
|
59
|
+
export declare const backendServiceMapper: ResourceMapper;
|
|
60
|
+
export declare const urlMapMapper: ResourceMapper;
|
|
61
|
+
export declare const targetHttpProxyMapper: ResourceMapper;
|
|
62
|
+
export declare const forwardingRuleMapper: ResourceMapper;
|
|
63
|
+
export declare const networkMapper: ResourceMapper;
|
|
64
|
+
export declare const subnetworkMapper: ResourceMapper;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=gcp-compute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcp-compute.d.ts","sourceRoot":"","sources":["../../../src/op/activities/gcp-compute.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9E,eAAO,MAAM,YAAY,mCAAmC,CAAC;AAI7D,KAAK,GAAG,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/D,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAqBnC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACnC;AAED,wGAAwG;AACxG,wBAAgB,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG3F;AAMD,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAKzD;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAmB5F;AAkBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMhG;AAED,oDAAoD;AACpD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CAWvE;AAED,2GAA2G;AAC3G,wBAAgB,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CA0BnE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,GAAG,CAStD;AAED,oKAAoK;AACpK,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CAW1E;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CAoC9E;AAED,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CA4BtE;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CAK/E;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,CAoB9E;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAe9B,CAAC;AAsBF,eAAO,MAAM,oBAAoB,gBAA4D,CAAC;AAC9F,eAAO,MAAM,gCAAgC,gBAAuF,CAAC;AACrI,eAAO,MAAM,oBAAoB,gBAAgF,CAAC;AAClH,eAAO,MAAM,YAAY,gBAAwD,CAAC;AAClF,eAAO,MAAM,qBAAqB,gBAAoF,CAAC;AACvH,eAAO,MAAM,oBAAoB,gBAAgF,CAAC;AAClH,eAAO,MAAM,aAAa,gBAAuF,CAAC;AAClH,eAAO,MAAM,gBAAgB,gBAAoE,CAAC"}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute Engine mappers for the Cloud Run load balancer (#4233): ComputeAddress,
|
|
3
|
+
* ComputeRegionNetworkEndpointGroup (serverless NEGs), ComputeBackendService,
|
|
4
|
+
* ComputeURLMap, ComputeTargetHTTPProxy and ComputeForwardingRule; and for a
|
|
5
|
+
* declared network (#4248): ComputeNetwork (always global) and
|
|
6
|
+
* ComputeSubnetwork (regional, by its CNRM `region`).
|
|
7
|
+
*
|
|
8
|
+
* Each maps a CNRM spec onto the compute v1 insert body. A resource is global
|
|
9
|
+
* or regional by its CNRM `location` (`"global"` or a region; a NEG has
|
|
10
|
+
* `region`), and that picks `projects/<p>/global/<collection>` or
|
|
11
|
+
* `projects/<p>/regions/<r>/<collection>`. `*Ref` fields become the referenced
|
|
12
|
+
* resource's partial URL, which compute accepts; a ref to a resource in the
|
|
13
|
+
* same manifest takes its scope from that resource's own spec, so a global
|
|
14
|
+
* backend service can point at a regional NEG.
|
|
15
|
+
*
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
import { GCP_RESOURCE_OWNERSHIP_KEYS } from "../../ownership.js";
|
|
22
|
+
import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
23
|
+
export const COMPUTE_HOST = "https://compute.googleapis.com";
|
|
24
|
+
const enc = encodeURIComponent;
|
|
25
|
+
function isObj(v) {
|
|
26
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
27
|
+
}
|
|
28
|
+
/** What a `*Ref` key points at: the CNRM kind (for a manifest lookup), the collection, and whether it is global only. */
|
|
29
|
+
const REF_TARGETS = {
|
|
30
|
+
addressRef: { kind: "ComputeAddress", collection: "addresses", scope: "either" },
|
|
31
|
+
networkEndpointGroupRef: { kind: "ComputeRegionNetworkEndpointGroup", collection: "networkEndpointGroups", scope: "regional" },
|
|
32
|
+
backendServiceRef: { kind: "ComputeBackendService", collection: "backendServices", scope: "either" },
|
|
33
|
+
backendBucketRef: { kind: "ComputeBackendBucket", collection: "backendBuckets", scope: "global" },
|
|
34
|
+
urlMapRef: { kind: "ComputeURLMap", collection: "urlMaps", scope: "either" },
|
|
35
|
+
targetHTTPProxyRef: { kind: "ComputeTargetHTTPProxy", collection: "targetHttpProxies", scope: "either" },
|
|
36
|
+
targetHTTPSProxyRef: { kind: "ComputeTargetHTTPSProxy", collection: "targetHttpsProxies", scope: "either" },
|
|
37
|
+
healthCheckRef: { kind: "ComputeHealthCheck", collection: "healthChecks", scope: "either" },
|
|
38
|
+
securityPolicyRef: { kind: "ComputeSecurityPolicy", collection: "securityPolicies", scope: "global" },
|
|
39
|
+
networkRef: { kind: "ComputeNetwork", collection: "networks", scope: "global" },
|
|
40
|
+
subnetworkRef: { kind: "ComputeSubnetwork", collection: "subnetworks", scope: "regional" },
|
|
41
|
+
};
|
|
42
|
+
/** The region or `"global"` a CNRM compute spec declares. A NEG names `region`; the rest `location`. */
|
|
43
|
+
export function scopeOf(spec, env = process.env) {
|
|
44
|
+
const loc = spec?.location ?? spec?.region;
|
|
45
|
+
return typeof loc === "string" && loc !== "" ? loc : (env.GCP_REGION || "us-central1");
|
|
46
|
+
}
|
|
47
|
+
function scopePath(project, scope) {
|
|
48
|
+
return scope === "global" ? `projects/${enc(project)}/global` : `projects/${enc(project)}/regions/${enc(scope)}`;
|
|
49
|
+
}
|
|
50
|
+
/** The compute name: CNRM's `resourceID` when set, else `metadata.name`. */
|
|
51
|
+
export function computeName(resource) {
|
|
52
|
+
const id = resource.spec?.resourceID;
|
|
53
|
+
const name = typeof id === "string" && id !== "" ? id : resource.metadata?.name;
|
|
54
|
+
if (!name)
|
|
55
|
+
throw new Error(`${resource.kind ?? "compute resource"} has no metadata.name`);
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The partial URL a `*Ref` resolves to. `external` is used as given when it is
|
|
60
|
+
* a path or URL; a bare name (or `name`) is scoped by the referenced resource's
|
|
61
|
+
* own spec when the manifest holds it, else by `fallbackScope`. Pure.
|
|
62
|
+
*/
|
|
63
|
+
export function refUrl(key, ref, ctx, fallbackScope) {
|
|
64
|
+
const target = REF_TARGETS[key];
|
|
65
|
+
if (!target)
|
|
66
|
+
throw new Error(`no compute collection for ${key}`);
|
|
67
|
+
if (typeof ref.external === "string" && ref.external !== "") {
|
|
68
|
+
if (ref.external.startsWith("projects/") || ref.external.startsWith("https://"))
|
|
69
|
+
return ref.external;
|
|
70
|
+
}
|
|
71
|
+
const name = ref.name ?? ref.external;
|
|
72
|
+
if (!name)
|
|
73
|
+
throw new Error(`${key} has no name or external`);
|
|
74
|
+
const declared = ctx.manifest?.find((r) => r.kind === target.kind && (r.metadata?.name === name || r.spec?.resourceID === name));
|
|
75
|
+
const scope = target.scope === "global"
|
|
76
|
+
? "global"
|
|
77
|
+
: declared
|
|
78
|
+
? scopeOf(declared.spec)
|
|
79
|
+
: target.scope === "regional" && fallbackScope === "global"
|
|
80
|
+
? scopeOf(undefined)
|
|
81
|
+
: fallbackScope;
|
|
82
|
+
const resolvedName = declared ? computeName(declared) : name;
|
|
83
|
+
return `${scopePath(ctx.project, scope)}/${target.collection}/${resolvedName}`;
|
|
84
|
+
}
|
|
85
|
+
/** Resolve the one `*Ref` key an object holds (`{ backendServiceRef: {...} }` → URL), or undefined. */
|
|
86
|
+
function oneRef(value, ctx, scope) {
|
|
87
|
+
if (!isObj(value))
|
|
88
|
+
return undefined;
|
|
89
|
+
for (const [k, v] of Object.entries(value)) {
|
|
90
|
+
if (REF_TARGETS[k] && isObj(v))
|
|
91
|
+
return refUrl(k, v, ctx, scope);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Copy the listed fields that are set. */
|
|
96
|
+
function pick(spec, keys) {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const k of keys)
|
|
99
|
+
if (spec[k] !== undefined)
|
|
100
|
+
out[k] = spec[k];
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The resource's labels GCP accepts, plus the ownership marker. Composites put
|
|
105
|
+
* Kubernetes-style keys (`app.kubernetes.io/component`) in `metadata.labels`,
|
|
106
|
+
* which a compute label key can't hold, so those are left out.
|
|
107
|
+
*/
|
|
108
|
+
export function computeLabels(labels) {
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [k, v] of Object.entries(labels ?? {})) {
|
|
111
|
+
if (/^[a-z][a-z0-9_-]{0,62}$/.test(k) && typeof v === "string" && /^[a-z0-9_-]{0,63}$/.test(v))
|
|
112
|
+
out[k] = v;
|
|
113
|
+
}
|
|
114
|
+
return { ...out, [GCP_RESOURCE_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE };
|
|
115
|
+
}
|
|
116
|
+
/** ComputeAddress → addresses.insert body. Pure. */
|
|
117
|
+
export function addressBody(resource, ctx) {
|
|
118
|
+
const spec = (resource.spec ?? {});
|
|
119
|
+
const scope = scopeOf(spec);
|
|
120
|
+
const body = {
|
|
121
|
+
name: computeName(resource),
|
|
122
|
+
...pick(spec, ["address", "addressType", "description", "ipVersion", "ipv6EndpointType", "networkTier", "prefixLength", "purpose"]),
|
|
123
|
+
labels: computeLabels(resource.metadata?.labels),
|
|
124
|
+
};
|
|
125
|
+
if (isObj(spec.networkRef))
|
|
126
|
+
body.network = refUrl("networkRef", spec.networkRef, ctx, scope);
|
|
127
|
+
if (isObj(spec.subnetworkRef))
|
|
128
|
+
body.subnetwork = refUrl("subnetworkRef", spec.subnetworkRef, ctx, scope);
|
|
129
|
+
return body;
|
|
130
|
+
}
|
|
131
|
+
/** ComputeRegionNetworkEndpointGroup → regionNetworkEndpointGroups.insert body (serverless NEGs). Pure. */
|
|
132
|
+
export function negBody(resource, ctx) {
|
|
133
|
+
const spec = (resource.spec ?? {});
|
|
134
|
+
const scope = scopeOf(spec);
|
|
135
|
+
const body = {
|
|
136
|
+
name: computeName(resource),
|
|
137
|
+
networkEndpointType: spec.networkEndpointType ?? "SERVERLESS",
|
|
138
|
+
...pick(spec, ["description", "pscTargetService"]),
|
|
139
|
+
};
|
|
140
|
+
const serviceName = (ref) => {
|
|
141
|
+
if (!isObj(ref))
|
|
142
|
+
return undefined;
|
|
143
|
+
const r = ref;
|
|
144
|
+
// A serverless NEG names the service, not its path.
|
|
145
|
+
return (r.external ?? r.name)?.split("/").pop();
|
|
146
|
+
};
|
|
147
|
+
if (isObj(spec.cloudRun)) {
|
|
148
|
+
const c = spec.cloudRun;
|
|
149
|
+
body.cloudRun = { ...pick(c, ["tag", "urlMask"]), ...(serviceName(c.serviceRef) ? { service: serviceName(c.serviceRef) } : {}) };
|
|
150
|
+
}
|
|
151
|
+
if (isObj(spec.cloudFunction)) {
|
|
152
|
+
const c = spec.cloudFunction;
|
|
153
|
+
body.cloudFunction = { ...pick(c, ["urlMask"]), ...(serviceName(c.functionRef) ? { function: serviceName(c.functionRef) } : {}) };
|
|
154
|
+
}
|
|
155
|
+
if (isObj(spec.appEngine))
|
|
156
|
+
body.appEngine = spec.appEngine;
|
|
157
|
+
if (isObj(spec.networkRef))
|
|
158
|
+
body.network = refUrl("networkRef", spec.networkRef, ctx, scope);
|
|
159
|
+
if (isObj(spec.subnetworkRef))
|
|
160
|
+
body.subnetwork = refUrl("subnetworkRef", spec.subnetworkRef, ctx, scope);
|
|
161
|
+
return body;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* ComputeNetwork → networks.insert body. A network is global. CNRM's
|
|
165
|
+
* `autoCreateSubnetworks` defaults to false, where compute's default is true,
|
|
166
|
+
* so it is always sent. Pure.
|
|
167
|
+
*/
|
|
168
|
+
export function networkBody(resource) {
|
|
169
|
+
const spec = (resource.spec ?? {});
|
|
170
|
+
const body = {
|
|
171
|
+
name: computeName(resource),
|
|
172
|
+
autoCreateSubnetworks: spec.autoCreateSubnetworks === true,
|
|
173
|
+
...pick(spec, ["description", "mtu", "networkFirewallPolicyEnforcementOrder"]),
|
|
174
|
+
};
|
|
175
|
+
if (typeof spec.routingMode === "string")
|
|
176
|
+
body.routingConfig = { routingMode: spec.routingMode };
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
/** ComputeSubnetwork → subnetworks.insert body. CNRM's `secondaryIpRange` and `logConfig` become compute's `secondaryIpRanges` and an enabled `logConfig`. Pure. */
|
|
180
|
+
export function subnetworkBody(resource, ctx) {
|
|
181
|
+
const spec = (resource.spec ?? {});
|
|
182
|
+
const scope = scopeOf(spec);
|
|
183
|
+
const body = {
|
|
184
|
+
name: computeName(resource),
|
|
185
|
+
...pick(spec, ["ipCidrRange", "description", "privateIpGoogleAccess", "purpose", "role", "stackType"]),
|
|
186
|
+
};
|
|
187
|
+
if (isObj(spec.networkRef))
|
|
188
|
+
body.network = refUrl("networkRef", spec.networkRef, ctx, scope);
|
|
189
|
+
if (Array.isArray(spec.secondaryIpRange))
|
|
190
|
+
body.secondaryIpRanges = spec.secondaryIpRange;
|
|
191
|
+
if (isObj(spec.logConfig))
|
|
192
|
+
body.logConfig = { enable: true, ...spec.logConfig };
|
|
193
|
+
return body;
|
|
194
|
+
}
|
|
195
|
+
/** ComputeBackendService → backendServices.insert body. Pure. */
|
|
196
|
+
export function backendServiceBody(resource, ctx) {
|
|
197
|
+
const spec = (resource.spec ?? {});
|
|
198
|
+
const scope = scopeOf(spec);
|
|
199
|
+
const body = {
|
|
200
|
+
name: computeName(resource),
|
|
201
|
+
...pick(spec, [
|
|
202
|
+
"affinityCookieTtlSec",
|
|
203
|
+
"cdnPolicy",
|
|
204
|
+
"compressionMode",
|
|
205
|
+
"customRequestHeaders",
|
|
206
|
+
"customResponseHeaders",
|
|
207
|
+
"description",
|
|
208
|
+
"loadBalancingScheme",
|
|
209
|
+
"localityLbPolicy",
|
|
210
|
+
"logConfig",
|
|
211
|
+
"portName",
|
|
212
|
+
"protocol",
|
|
213
|
+
"sessionAffinity",
|
|
214
|
+
"timeoutSec",
|
|
215
|
+
]),
|
|
216
|
+
};
|
|
217
|
+
if (spec.enableCdn !== undefined)
|
|
218
|
+
body.enableCDN = spec.enableCdn;
|
|
219
|
+
if (spec.connectionDrainingTimeoutSec !== undefined)
|
|
220
|
+
body.connectionDraining = { drainingTimeoutSec: spec.connectionDrainingTimeoutSec };
|
|
221
|
+
if (Array.isArray(spec.backend)) {
|
|
222
|
+
body.backends = spec.backend.map((b) => {
|
|
223
|
+
const { group, ...rest } = b;
|
|
224
|
+
const groupUrl = isObj(group) ? oneRef(group, ctx, scope) ?? group.instanceGroupRef?.external : undefined;
|
|
225
|
+
return { ...rest, ...(groupUrl ? { group: groupUrl } : {}) };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(spec.healthChecks)) {
|
|
229
|
+
body.healthChecks = spec.healthChecks.map((h) => oneRef(h, ctx, scope)).filter((u) => !!u);
|
|
230
|
+
}
|
|
231
|
+
if (isObj(spec.securityPolicyRef))
|
|
232
|
+
body.securityPolicy = refUrl("securityPolicyRef", spec.securityPolicyRef, ctx, scope);
|
|
233
|
+
else if (typeof spec.securityPolicy === "string")
|
|
234
|
+
body.securityPolicy = spec.securityPolicy;
|
|
235
|
+
return body;
|
|
236
|
+
}
|
|
237
|
+
/** ComputeURLMap → urlMaps.insert body. Pure. */
|
|
238
|
+
export function urlMapBody(resource, ctx) {
|
|
239
|
+
const spec = (resource.spec ?? {});
|
|
240
|
+
const scope = scopeOf(spec);
|
|
241
|
+
const service = (v) => oneRef(v, ctx, scope);
|
|
242
|
+
const body = {
|
|
243
|
+
name: computeName(resource),
|
|
244
|
+
...pick(spec, ["description", "defaultUrlRedirect", "headerAction"]),
|
|
245
|
+
};
|
|
246
|
+
const def = service(spec.defaultService);
|
|
247
|
+
if (def)
|
|
248
|
+
body.defaultService = def;
|
|
249
|
+
if (Array.isArray(spec.hostRule))
|
|
250
|
+
body.hostRules = spec.hostRule;
|
|
251
|
+
if (Array.isArray(spec.pathMatcher)) {
|
|
252
|
+
body.pathMatchers = spec.pathMatcher.map((m) => {
|
|
253
|
+
const { defaultService, pathRule, ...rest } = m;
|
|
254
|
+
const out = { ...rest };
|
|
255
|
+
const d = service(defaultService);
|
|
256
|
+
if (d)
|
|
257
|
+
out.defaultService = d;
|
|
258
|
+
if (Array.isArray(pathRule)) {
|
|
259
|
+
out.pathRules = pathRule.map((r) => {
|
|
260
|
+
const { service: s, ...rr } = r;
|
|
261
|
+
const u = service(s);
|
|
262
|
+
return { ...rr, ...(u ? { service: u } : {}) };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return body;
|
|
269
|
+
}
|
|
270
|
+
/** ComputeTargetHTTPProxy → targetHttpProxies.insert body. Pure. */
|
|
271
|
+
export function targetHttpProxyBody(resource, ctx) {
|
|
272
|
+
const spec = (resource.spec ?? {});
|
|
273
|
+
const body = { name: computeName(resource), ...pick(spec, ["description", "proxyBind", "httpKeepAliveTimeoutSec"]) };
|
|
274
|
+
if (isObj(spec.urlMapRef))
|
|
275
|
+
body.urlMap = refUrl("urlMapRef", spec.urlMapRef, ctx, scopeOf(spec));
|
|
276
|
+
return body;
|
|
277
|
+
}
|
|
278
|
+
/** ComputeForwardingRule → forwardingRules.insert body. Pure. */
|
|
279
|
+
export function forwardingRuleBody(resource, ctx) {
|
|
280
|
+
const spec = (resource.spec ?? {});
|
|
281
|
+
const scope = scopeOf(spec);
|
|
282
|
+
const body = {
|
|
283
|
+
name: computeName(resource),
|
|
284
|
+
...pick(spec, ["allowGlobalAccess", "description", "ipVersion", "isMirroringCollector", "loadBalancingScheme", "networkTier", "portRange", "ports", "allPorts"]),
|
|
285
|
+
labels: computeLabels(resource.metadata?.labels),
|
|
286
|
+
};
|
|
287
|
+
if (spec.ipProtocol !== undefined)
|
|
288
|
+
body.IPProtocol = spec.ipProtocol;
|
|
289
|
+
if (isObj(spec.ipAddress)) {
|
|
290
|
+
const ip = spec.ipAddress;
|
|
291
|
+
const addr = isObj(ip.addressRef) ? refUrl("addressRef", ip.addressRef, ctx, scope) : ip.ip;
|
|
292
|
+
if (addr)
|
|
293
|
+
body.IPAddress = addr;
|
|
294
|
+
}
|
|
295
|
+
const target = oneRef(spec.target, ctx, scope);
|
|
296
|
+
if (target)
|
|
297
|
+
body.target = target;
|
|
298
|
+
if (isObj(spec.backendServiceRef))
|
|
299
|
+
body.backendService = refUrl("backendServiceRef", spec.backendServiceRef, ctx, scope);
|
|
300
|
+
if (isObj(spec.networkRef))
|
|
301
|
+
body.network = refUrl("networkRef", spec.networkRef, ctx, scope);
|
|
302
|
+
if (isObj(spec.subnetworkRef))
|
|
303
|
+
body.subnetwork = refUrl("subnetworkRef", spec.subnetworkRef, ctx, scope);
|
|
304
|
+
return body;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A compute `Operation`: the insert or delete returns one with a `selfLink`,
|
|
308
|
+
* polled until `status: "DONE"`. The link is rebased onto `base`, so an
|
|
309
|
+
* endpoint override (floci-gcp) is polled too. A create that returned the
|
|
310
|
+
* resource itself has nothing to poll.
|
|
311
|
+
*/
|
|
312
|
+
export const computeOperation = {
|
|
313
|
+
pollUrl(body, { base }) {
|
|
314
|
+
const op = body;
|
|
315
|
+
const link = op?.selfLink;
|
|
316
|
+
if (!link || !link.includes("/operations/"))
|
|
317
|
+
return undefined;
|
|
318
|
+
const at = link.indexOf("/compute/v1/");
|
|
319
|
+
return at >= 0 ? `${base}${link.slice(at)}` : link;
|
|
320
|
+
},
|
|
321
|
+
isDone(body) {
|
|
322
|
+
return body?.status === "DONE";
|
|
323
|
+
},
|
|
324
|
+
error(body) {
|
|
325
|
+
const errors = body?.error?.errors;
|
|
326
|
+
return errors?.length ? errors.map((e) => e.message ?? e.code ?? "error").join("; ") : undefined;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
function computeMapper(kind, collection, build, regionalCollection = collection, scopeFor = (spec) => scopeOf(spec)) {
|
|
330
|
+
return {
|
|
331
|
+
kind,
|
|
332
|
+
defaultHost: COMPUTE_HOST,
|
|
333
|
+
operation: computeOperation,
|
|
334
|
+
plan(resource, ctx) {
|
|
335
|
+
const scope = scopeFor(resource.spec);
|
|
336
|
+
const coll = `${ctx.base}/compute/v1/${scopePath(ctx.project, scope)}/${scope === "global" ? collection : regionalCollection}`;
|
|
337
|
+
const name = computeName(resource);
|
|
338
|
+
return { getUrl: `${coll}/${enc(name)}`, create: { method: "POST", url: coll, body: build(resource, ctx) } };
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export const computeAddressMapper = computeMapper("ComputeAddress", "addresses", addressBody);
|
|
343
|
+
export const regionNetworkEndpointGroupMapper = computeMapper("ComputeRegionNetworkEndpointGroup", "networkEndpointGroups", negBody);
|
|
344
|
+
export const backendServiceMapper = computeMapper("ComputeBackendService", "backendServices", backendServiceBody);
|
|
345
|
+
export const urlMapMapper = computeMapper("ComputeURLMap", "urlMaps", urlMapBody);
|
|
346
|
+
export const targetHttpProxyMapper = computeMapper("ComputeTargetHTTPProxy", "targetHttpProxies", targetHttpProxyBody);
|
|
347
|
+
export const forwardingRuleMapper = computeMapper("ComputeForwardingRule", "forwardingRules", forwardingRuleBody);
|
|
348
|
+
export const networkMapper = computeMapper("ComputeNetwork", "networks", networkBody, "networks", () => "global");
|
|
349
|
+
export const subnetworkMapper = computeMapper("ComputeSubnetwork", "subnetworks", subnetworkBody);
|