@intentius/chant-lexicon-gcp 0.44.14 → 0.46.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.
Files changed (50) hide show
  1. package/dist/codegen/docs.d.ts.map +1 -1
  2. package/dist/composites/gke-cluster.d.ts +3 -2
  3. package/dist/composites/gke-cluster.d.ts.map +1 -1
  4. package/dist/composites/gke-crdb-region.d.ts.map +1 -1
  5. package/dist/describe-resources.d.ts.map +1 -1
  6. package/dist/integrity.json +4 -4
  7. package/dist/manifest.json +1 -1
  8. package/dist/op/activities/gcp-apply.d.ts +13 -1
  9. package/dist/op/activities/gcp-apply.d.ts.map +1 -1
  10. package/dist/op/activities/index.d.ts +1 -1
  11. package/dist/op/activities/index.d.ts.map +1 -1
  12. package/dist/skills/chant-gcp-patterns.md +1 -0
  13. package/dist/skills/chant-gcp.md +1 -0
  14. package/package.json +2 -2
  15. package/src/codegen/docs.ts +0 -726
  16. package/src/composites/composites.test.ts +9 -9
  17. package/src/composites/gke-cluster.ts +4 -5
  18. package/src/composites/gke-crdb-region.ts +1 -0
  19. package/src/describe-resources.ts +4 -1
  20. package/src/lifecycle-integration.test.ts +28 -0
  21. package/src/lint/post-synth/post-synth.test.ts +2 -3
  22. package/src/lint/post-synth/wgc101.test.ts +2 -3
  23. package/src/lint/post-synth/wgc102.test.ts +2 -3
  24. package/src/lint/post-synth/wgc103.test.ts +2 -3
  25. package/src/lint/post-synth/wgc104.test.ts +2 -3
  26. package/src/lint/post-synth/wgc105.test.ts +2 -3
  27. package/src/lint/post-synth/wgc106.test.ts +2 -3
  28. package/src/lint/post-synth/wgc107.test.ts +2 -3
  29. package/src/lint/post-synth/wgc108.test.ts +2 -3
  30. package/src/lint/post-synth/wgc109.test.ts +2 -3
  31. package/src/lint/post-synth/wgc110.test.ts +2 -3
  32. package/src/lint/post-synth/wgc111.test.ts +2 -3
  33. package/src/lint/post-synth/wgc112.test.ts +2 -3
  34. package/src/lint/post-synth/wgc113.test.ts +2 -3
  35. package/src/lint/post-synth/wgc201.test.ts +2 -3
  36. package/src/lint/post-synth/wgc202.test.ts +2 -3
  37. package/src/lint/post-synth/wgc203.test.ts +2 -3
  38. package/src/lint/post-synth/wgc204.test.ts +2 -3
  39. package/src/lint/post-synth/wgc301.test.ts +2 -3
  40. package/src/lint/post-synth/wgc302.test.ts +2 -3
  41. package/src/lint/post-synth/wgc303.test.ts +2 -3
  42. package/src/lint/post-synth/wgc401.test.ts +2 -3
  43. package/src/lint/post-synth/wgc402.test.ts +2 -3
  44. package/src/lint/post-synth/wgc403.test.ts +2 -3
  45. package/src/op/activities/gcp-apply.test.ts +39 -0
  46. package/src/op/activities/gcp-apply.ts +20 -3
  47. package/src/op/activities/index.ts +1 -0
  48. package/src/serializer.test.ts +3 -3
  49. package/src/skills/chant-gcp-patterns.md +1 -0
  50. package/src/skills/chant-gcp.md +1 -0
@@ -19,35 +19,35 @@ function p(member: unknown): Record<string, any> {
19
19
 
20
20
  describe("GkeCluster", () => {
21
21
  test("returns cluster and node pool", () => {
22
- const result = GkeCluster({ name: "my-cluster" });
22
+ const result = GkeCluster({ name: "my-cluster", projectId: "proj" });
23
23
  expect(result.cluster).toBeDefined();
24
24
  expect(result.nodePool).toBeDefined();
25
25
  });
26
26
 
27
27
  test("includes common labels", () => {
28
- const result = GkeCluster({ name: "my-cluster" });
28
+ const result = GkeCluster({ name: "my-cluster", projectId: "proj" });
29
29
  const meta = p(result.cluster).metadata;
30
30
  expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
31
31
  });
32
32
 
33
33
  test("node pool references cluster", () => {
34
- const result = GkeCluster({ name: "my-cluster" });
34
+ const result = GkeCluster({ name: "my-cluster", projectId: "proj" });
35
35
  expect(p(result.nodePool).clusterRef.name).toBe("my-cluster");
36
36
  });
37
37
 
38
38
  test("respects maxNodeCount", () => {
39
- const result = GkeCluster({ name: "c", maxNodeCount: 20 });
39
+ const result = GkeCluster({ name: "c", projectId: "proj", maxNodeCount: 20 });
40
40
  expect(p(result.nodePool).autoscaling.maxNodeCount).toBe(20);
41
41
  });
42
42
 
43
43
  test("sets namespace when provided", () => {
44
- const result = GkeCluster({ name: "c", namespace: "infra" });
44
+ const result = GkeCluster({ name: "c", projectId: "proj", namespace: "infra" });
45
45
  expect(p(result.cluster).metadata.namespace).toBe("infra");
46
46
  expect(p(result.nodePool).metadata.namespace).toBe("infra");
47
47
  });
48
48
 
49
49
  test("enables workload identity by default", () => {
50
- const result = GkeCluster({ name: "c" });
50
+ const result = GkeCluster({ name: "c", projectId: "proj" });
51
51
  expect(p(result.cluster).workloadIdentityConfig).toBeDefined();
52
52
  expect(p(result.nodePool).nodeConfig.workloadMetadataConfig).toBeDefined();
53
53
  });
@@ -59,13 +59,13 @@ describe("GkeCluster", () => {
59
59
  );
60
60
  });
61
61
 
62
- test("workloadPool falls back to GCP_PROJECT_ID env var", () => {
62
+ test("ignores GCP_PROJECT_ID env var projectId comes from props only", () => {
63
63
  const prev = process.env.GCP_PROJECT_ID;
64
64
  process.env.GCP_PROJECT_ID = "env-project-456";
65
65
  try {
66
- const result = GkeCluster({ name: "c" });
66
+ const result = GkeCluster({ name: "c", projectId: "explicit-project" });
67
67
  expect(p(result.cluster).workloadIdentityConfig.workloadPool).toBe(
68
- "env-project-456.svc.id.goog",
68
+ "explicit-project.svc.id.goog",
69
69
  );
70
70
  } finally {
71
71
  if (prev === undefined) delete process.env.GCP_PROJECT_ID;
@@ -11,8 +11,8 @@ import { GKECluster, NodePool } from "../generated";
11
11
  export interface GkeClusterProps {
12
12
  /** Cluster name. */
13
13
  name: string;
14
- /** GCP project ID for workload identity pool. Falls back to GCP_PROJECT_ID env var. */
15
- projectId?: string;
14
+ /** GCP project ID for the workload identity pool. */
15
+ projectId: string;
16
16
  /** GCP region for regional cluster (default: uses GCP.Region). */
17
17
  location?: string;
18
18
  /** Initial node count per zone (default: 1). */
@@ -75,6 +75,7 @@ export interface GkeClusterProps {
75
75
  *
76
76
  * const { cluster, nodePool } = GkeCluster({
77
77
  * name: "my-cluster",
78
+ * projectId: "my-project",
78
79
  * location: "us-central1",
79
80
  * maxNodeCount: 10,
80
81
  * });
@@ -83,7 +84,7 @@ export interface GkeClusterProps {
83
84
  export const GkeCluster = Composite((props: GkeClusterProps) => {
84
85
  const {
85
86
  name,
86
- projectId: rawProjectId,
87
+ projectId,
87
88
  location,
88
89
  initialNodeCount = 1,
89
90
  machineType = "e2-medium",
@@ -103,8 +104,6 @@ export const GkeCluster = Composite((props: GkeClusterProps) => {
103
104
  defaults: defs,
104
105
  } = props;
105
106
 
106
- const projectId = rawProjectId ?? process.env.GCP_PROJECT_ID ?? "PROJECT_ID";
107
-
108
107
  const commonLabels: Record<string, string> = {
109
108
  "app.kubernetes.io/name": name,
110
109
  "app.kubernetes.io/managed-by": "chant",
@@ -125,6 +125,7 @@ export const GkeCrdbRegion = Composite<GkeCrdbRegionConfig>((props) => {
125
125
 
126
126
  const { cluster, nodePool, defaultPool } = GkeCluster({
127
127
  name: clusterName,
128
+ projectId: project,
128
129
  location: region,
129
130
  machineType,
130
131
  diskSizeGb,
@@ -38,7 +38,7 @@
38
38
 
39
39
  import type { ObservationResult, ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
40
40
  import { observation } from "@intentius/chant/observation";
41
- import { hasOwnershipMarker, classifyOwnership, type ChannelKeys } from "@intentius/chant/ownership";
41
+ import { hasOwnershipMarker, classifyOwnership, readOwnership, type ChannelKeys } from "@intentius/chant/ownership";
42
42
  import { getResource, mapperForKind, GcpReadError, isNotFound, type GcpReadClientOptions } from "./api/read-client";
43
43
  import { resolveGcpProject, parseManifest } from "./op/activities/gcp-apply";
44
44
  import { resolveGVK } from "./serializer";
@@ -272,6 +272,9 @@ export async function describeResources(options: {
272
272
  status: statusFromRest(obj),
273
273
  lastUpdated: obj.updated ?? obj.timeCreated,
274
274
  ownership: obj.labels == null ? "unknown" : classifyOwnership(obj.labels, GCP_OWNERSHIP_LABEL_KEYS),
275
+ // Marker identity (#1222): stack/env from the resource's own labels.
276
+ // Undefined when the payload carries no labels or no managed-by marker.
277
+ marker: readOwnership(obj.labels ?? undefined, GCP_OWNERSHIP_LABEL_KEYS),
275
278
  attributes: pruneUndefined({
276
279
  labels: obj.labels ?? undefined,
277
280
  selfLink: obj.selfLink,
@@ -176,5 +176,33 @@ describeObservationConformance({
176
176
  });
177
177
  },
178
178
  },
179
+ {
180
+ name: "a marker-stamped resource surfaces its stack/env identity; a foreign one surfaces none (#1222)",
181
+ declared: ["ownedBucket", "dataBucket"],
182
+ expectPresent: ["ownedBucket", "dataBucket"],
183
+ expectMarker: { ownedBucket: { stack: "shop", env: "prod" } },
184
+ expectNoMarker: ["dataBucket"],
185
+ run: () => {
186
+ fetchMock.mockImplementation((url: string) =>
187
+ Promise.resolve(
188
+ String(url).includes("owned-bucket")
189
+ ? restReply(200, {
190
+ id: "b/owned-bucket",
191
+ labels: { "managed-by": "chant", "chant-stack": "shop", "chant-env": "prod" },
192
+ })
193
+ : restReply(200, { id: "b/data-bucket", labels: { team: "data" } }),
194
+ ),
195
+ );
196
+ return gcpPlugin.describeResources!({
197
+ environment: "prod",
198
+ buildOutput: "",
199
+ entityNames: ["ownedBucket", "dataBucket"],
200
+ entities: new Map([
201
+ ["ownedBucket", { entityType: "GCP::Storage::Bucket", props: { metadata: { name: "owned-bucket" } } }],
202
+ ["dataBucket", { entityType: "GCP::Storage::Bucket", props: { metadata: { name: "data-bucket" } } }],
203
+ ]),
204
+ });
205
+ },
206
+ },
179
207
  ],
180
208
  });
@@ -1,4 +1,5 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc101 } from "./wgc101";
3
4
  import { wgc102 } from "./wgc102";
4
5
  import { wgc103 } from "./wgc103";
@@ -24,9 +25,7 @@ import { wgc402 } from "./wgc402";
24
25
  import { wgc403 } from "./wgc403";
25
26
 
26
27
  function makeCtx(yaml: string) {
27
- return {
28
- outputs: new Map([["gcp", yaml]]),
29
- };
28
+ return makePostSynthCtx("gcp", yaml);
30
29
  }
31
30
 
32
31
  // ── WGC101: Missing encryption ─────────────────────────────────────
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc101 } from "./wgc101";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC101: missing encryption", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc102 } from "./wgc102";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC102: public IAM in output", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc103 } from "./wgc103";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC103: missing project annotation", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc104 } from "./wgc104";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC104: missing uniform bucket access", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc105 } from "./wgc105";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC105: public Cloud SQL", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc106 } from "./wgc106";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC106: missing deletion policy", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc107 } from "./wgc107";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC107: missing versioning", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc108 } from "./wgc108";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC108: missing backup configuration", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc109 } from "./wgc109";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC109: open firewall", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc110 } from "./wgc110";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC110: missing key rotation", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc111 } from "./wgc111";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC111: dangling resource reference", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc112 } from "./wgc112";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC112: missing or invalid apiVersion", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc113 } from "./wgc113";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC113: alpha API version", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc201 } from "./wgc201";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC201: missing managed-by label", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc202 } from "./wgc202";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC202: missing workload identity", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc203 } from "./wgc203";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC203: cloud-platform OAuth scope", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc204 } from "./wgc204";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC204: missing shielded VM config", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc301 } from "./wgc301";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC301: no audit logging", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc302 } from "./wgc302";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC302: service API not enabled", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc303 } from "./wgc303";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC303: missing VPC Service Controls", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc401 } from "./wgc401";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC401: unknown spec field", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc402 } from "./wgc402";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC402: missing required field", () => {
@@ -1,10 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
2
3
  import { wgc403 } from "./wgc403";
3
4
 
4
5
  function makeCtx(yaml: string) {
5
- return {
6
- outputs: new Map([["gcp", yaml]]),
7
- };
6
+ return makePostSynthCtx("gcp", yaml);
8
7
  }
9
8
 
10
9
  describe("WGC403: type/structure mismatch", () => {
@@ -6,6 +6,7 @@ import {
6
6
  pubSubTopicBody,
7
7
  cloudRunServiceBody,
8
8
  resolveGcpProject,
9
+ resolveGcpEndpoint,
9
10
  applyResource,
10
11
  deleteResource,
11
12
  waitForOperation,
@@ -235,6 +236,44 @@ describe("resolveGcpProject (#711)", () => {
235
236
  });
236
237
  });
237
238
 
239
+ describe("resolveGcpEndpoint (#1449)", () => {
240
+ test("an explicit endpoint arg wins", () => {
241
+ expect(
242
+ resolveGcpEndpoint({ endpoint: "http://arg:1" }, { GCP_ENDPOINT_URL: "http://env:2" } as NodeJS.ProcessEnv),
243
+ ).toBe("http://arg:1");
244
+ });
245
+
246
+ test("falls back to GCP_ENDPOINT_URL — the same variable the read path honours", () => {
247
+ expect(resolveGcpEndpoint({}, { GCP_ENDPOINT_URL: "http://localhost:4588" } as NodeJS.ProcessEnv))
248
+ .toBe("http://localhost:4588");
249
+ });
250
+
251
+ test("undefined when neither is set — each kind then uses its real-GCP host", () => {
252
+ expect(resolveGcpEndpoint({}, {} as NodeJS.ProcessEnv)).toBeUndefined();
253
+ });
254
+
255
+ test("gcpApply honours GCP_ENDPOINT_URL, so nativeApply({ target: 'gcp' }) reaches floci-gcp without passing an endpoint", async () => {
256
+ const path = "/tmp/chant-1449-endpoint-test.yaml";
257
+ writeFileSync(path, JSON.stringify(BUCKET));
258
+ const prev = process.env.GCP_ENDPOINT_URL;
259
+ process.env.GCP_ENDPOINT_URL = "http://localhost:4588";
260
+ try {
261
+ const urls: string[] = [];
262
+ const http: GcpHttp = async (_method, url) => {
263
+ urls.push(url);
264
+ return { status: 200, text: "{}" };
265
+ };
266
+ await gcpApply({ manifestPath: path, project: "p" }, undefined, http);
267
+ expect(urls.length).toBeGreaterThan(0);
268
+ for (const url of urls) expect(url.startsWith("http://localhost:4588/")).toBe(true);
269
+ } finally {
270
+ if (prev === undefined) delete process.env.GCP_ENDPOINT_URL;
271
+ else process.env.GCP_ENDPOINT_URL = prev;
272
+ unlinkSync(path);
273
+ }
274
+ });
275
+ });
276
+
238
277
  describe("applyResource create/update (#706)", () => {
239
278
  function recorder(getStatus: number): { http: GcpHttp; calls: Array<{ method: string; url: string }> } {
240
279
  const calls: Array<{ method: string; url: string }> = [];
@@ -427,6 +427,21 @@ export function orderByReferences(resources: GcpResource[]): GcpResource[] {
427
427
 
428
428
  // ── Resolution + HTTP ─────────────────────────────────────────────────────────
429
429
 
430
+ /**
431
+ * Resolve the endpoint override (#1449): an explicit `endpoint` arg wins, then
432
+ * the `GCP_ENDPOINT_URL` env — the same variable the read path honours
433
+ * (`describe-resources`, floci-gcp), so an apply lands wherever `--live` is
434
+ * already looking. `undefined` means no override: each kind falls back to its
435
+ * real-GCP host. The same rule the aws applier applies to `AWS_ENDPOINT_URL`
436
+ * (#1694) and the fly applier to `FLY_FLAPS_BASE_URL`. Pure.
437
+ */
438
+ export function resolveGcpEndpoint(
439
+ args: { endpoint?: string } = {},
440
+ env: NodeJS.ProcessEnv = process.env,
441
+ ): string | undefined {
442
+ return args.endpoint || env.GCP_ENDPOINT_URL || undefined;
443
+ }
444
+
430
445
  /** Resolve the project: `GOOGLE_CLOUD_PROJECT` env, else the CNRM project-id annotation. Pure. */
431
446
  export function resolveGcpProject(resource: GcpResource, env: NodeJS.ProcessEnv = process.env): string {
432
447
  const project = env.GOOGLE_CLOUD_PROJECT || resource.metadata?.annotations?.[PROJECT_ID_ANNOTATION];
@@ -585,7 +600,8 @@ export function parseManifest(content: string, path: string): GcpResource[] {
585
600
  export interface GcpApplyArgs {
586
601
  /** Path to a built CNRM manifest (YAML multi-doc or JSON). */
587
602
  manifestPath: string;
588
- /** GCS/GCP endpoint override for all kinds (e.g. floci-gcp `:4588`). Default: each kind's real-GCP host. */
603
+ /** GCS/GCP endpoint override for all kinds (e.g. floci-gcp `:4588`).
604
+ * Default: `GCP_ENDPOINT_URL` env, else each kind's real-GCP host. */
589
605
  endpoint?: string;
590
606
  /** Project override. Default: `GOOGLE_CLOUD_PROJECT` env / CNRM annotation. */
591
607
  project?: string;
@@ -742,8 +758,9 @@ export async function gcpApply(
742
758
  notPrunable: GcpNotPrunable[];
743
759
  }> {
744
760
  const resources = orderByReferences(parseManifest(readFileSync(args.manifestPath, "utf8"), args.manifestPath));
761
+ const endpoint = resolveGcpEndpoint(args);
745
762
  const resolve = (mapper: ResourceMapper, resource?: GcpResource) => ({
746
- base: (args.endpoint ?? mapper.defaultHost).replace(/\/$/, ""),
763
+ base: (endpoint ?? mapper.defaultHost).replace(/\/$/, ""),
747
764
  project: args.project ?? (resource ? resolveGcpProject(resource) : ""),
748
765
  });
749
766
 
@@ -831,7 +848,7 @@ export async function gcpDelete(
831
848
  notAttempted.push({ kind, name: r.metadata?.name ?? "?", reason: "unsupported-kind" });
832
849
  continue;
833
850
  }
834
- const base = (args.endpoint ?? mapper.defaultHost).replace(/\/$/, "");
851
+ const base = (resolveGcpEndpoint(args) ?? mapper.defaultHost).replace(/\/$/, "");
835
852
  const project = args.project ?? resolveGcpProject(r);
836
853
  safeHeartbeat({ step: "gcpDelete", kind: mapper.kind, name: r.metadata?.name });
837
854
  const result = await deleteResource(mapper, r, { base, project }, http, signal);
@@ -32,6 +32,7 @@ export {
32
32
  type GcpNotPrunable,
33
33
  chantOwnershipLabels,
34
34
  isChantOwned,
35
+ resolveGcpEndpoint,
35
36
  resolveGcpProject,
36
37
  parseManifest,
37
38
  } from "./gcp-apply";
@@ -216,7 +216,7 @@ describe("gcpSerializer", () => {
216
216
  }),
217
217
  );
218
218
 
219
- const result = gcpSerializer.serialize(entities);
219
+ const result = gcpSerializer.serialize(entities) as string;
220
220
  expect(result).toContain("env: prod");
221
221
  // Should not contain "dev" since explicit overrides
222
222
  const envLines = result.split("\n").filter((l: string) => l.includes("env:"));
@@ -233,7 +233,7 @@ describe("gcpSerializer", () => {
233
233
  }),
234
234
  );
235
235
 
236
- const result = gcpSerializer.serialize(entities);
236
+ const result = gcpSerializer.serialize(entities) as string;
237
237
  // Only one document — property entities should not appear as separate docs
238
238
  expect(result.split("---").length).toBeLessThanOrEqual(2);
239
239
  });
@@ -264,7 +264,7 @@ describe("gcpSerializer", () => {
264
264
  }),
265
265
  );
266
266
 
267
- const result = gcpSerializer.serialize(entities);
267
+ const result = gcpSerializer.serialize(entities) as string;
268
268
  const lines = result.split("\n");
269
269
  const keyLines = lines.filter((l: string) => /^\w+:/.test(l));
270
270
  const keys = keyLines.map((l: string) => l.split(":")[0]);
@@ -127,6 +127,7 @@ import { GkeCluster } from "@intentius/chant-lexicon-gcp";
127
127
 
128
128
  const { cluster, nodePool } = GkeCluster({
129
129
  name: "my-cluster",
130
+ projectId: "my-project",
130
131
  location: "us-central1",
131
132
  machineType: "e2-medium",
132
133
  minNodeCount: 1,