@intentius/chant-lexicon-gcp 0.124.38 → 0.124.40
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 +7 -0
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/api/read-client.js +24 -9
- package/dist/components/capability-plugin.d.ts +14 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/components/capability-plugin.js +21 -0
- package/dist/components/gcp-apply.d.ts +104 -0
- package/dist/components/gcp-apply.d.ts.map +1 -0
- package/dist/components/gcp-apply.js +335 -0
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/catalog.js +187 -0
- package/dist/composites/index.d.ts +4 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/index.js +2 -0
- package/dist/composites/private-egress-network.d.ts +42 -0
- package/dist/composites/private-egress-network.d.ts.map +1 -0
- package/dist/composites/private-egress-network.js +48 -0
- package/dist/composites/public-cloud-run.d.ts +85 -0
- package/dist/composites/public-cloud-run.d.ts.map +1 -0
- package/dist/composites/public-cloud-run.js +144 -0
- package/dist/deep-observe.d.ts +5 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +34 -2
- package/dist/health.d.ts +26 -2
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +85 -6
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/op/activities/gcp-apply.d.ts +61 -4
- package/dist/op/activities/gcp-apply.d.ts.map +1 -1
- package/dist/op/activities/gcp-apply.js +54 -22
- package/dist/op/activities/gcp-compute.d.ts +9 -0
- package/dist/op/activities/gcp-compute.d.ts.map +1 -1
- package/dist/op/activities/gcp-compute.js +107 -11
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -2
- package/dist/skills/chant-gcp.md +2 -0
- package/package.json +2 -2
- package/src/api/read-client.ts +23 -8
- package/src/components/capability-plugin.ts +25 -0
- package/src/components/gcp-apply-floci.e2e.test.ts +80 -0
- package/src/components/gcp-apply.test.ts +259 -0
- package/src/components/gcp-apply.ts +406 -0
- package/src/composites/catalog.ts +187 -0
- package/src/composites/cloud-run-lb.test.ts +93 -0
- package/src/composites/index.ts +4 -0
- package/src/composites/private-egress-network.ts +82 -0
- package/src/composites/public-cloud-run.ts +245 -0
- package/src/deep-observe.test.ts +46 -0
- package/src/deep-observe.ts +38 -2
- package/src/health.test.ts +65 -1
- package/src/health.ts +88 -11
- package/src/index.ts +13 -1
- package/src/op/activities/gcp-apply.ts +95 -24
- package/src/op/activities/gcp-compute-reconcile.test.ts +150 -0
- package/src/op/activities/gcp-compute.test.ts +4 -2
- package/src/op/activities/gcp-compute.ts +129 -11
- package/src/plugin.test.ts +10 -0
- package/src/plugin.ts +6 -2
- package/src/skills/chant-gcp.md +2 -0
|
@@ -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).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { gcpPlugin } from "./plugin";
|
|
3
3
|
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
4
|
+
import { emulatorsOf, endpointEnvVars } from "@intentius/chant/op";
|
|
4
5
|
|
|
5
6
|
describe("gcpPlugin", () => {
|
|
6
7
|
// ── Basic interface ────────────────────────────────────────────────
|
|
@@ -254,3 +255,12 @@ describe("gcpPlugin", () => {
|
|
|
254
255
|
});
|
|
255
256
|
});
|
|
256
257
|
});
|
|
258
|
+
|
|
259
|
+
// #4281: `chant components pr-plan`, `pr-apply` and `run --components` apply an
|
|
260
|
+
// environment's declared endpoint to the vars the plugin's emulator names. `gcp-apply`
|
|
261
|
+
// reads GCP_ENDPOINT_URL, so the plugin has to name it.
|
|
262
|
+
describe("gcpPlugin endpoint var (#4281)", () => {
|
|
263
|
+
test("the emulator names GCP_ENDPOINT_URL, the var gcp-apply reads", () => {
|
|
264
|
+
expect(emulatorsOf(gcpPlugin.emulator).flatMap((cap) => endpointEnvVars(cap))).toEqual(["GCP_ENDPOINT_URL"]);
|
|
265
|
+
});
|
|
266
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -411,8 +411,12 @@ export const bucket = new StorageBucket({
|
|
|
411
411
|
|
|
412
412
|
deepNormalizationHooks: gcpDeepNormalizationHooks,
|
|
413
413
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
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");
|
package/src/skills/chant-gcp.md
CHANGED
|
@@ -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
|
|