@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.
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/composites/gke-cluster.d.ts +3 -2
- package/dist/composites/gke-cluster.d.ts.map +1 -1
- package/dist/composites/gke-crdb-region.d.ts.map +1 -1
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/manifest.json +1 -1
- package/dist/op/activities/gcp-apply.d.ts +13 -1
- package/dist/op/activities/gcp-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/skills/chant-gcp-patterns.md +1 -0
- package/dist/skills/chant-gcp.md +1 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +0 -726
- package/src/composites/composites.test.ts +9 -9
- package/src/composites/gke-cluster.ts +4 -5
- package/src/composites/gke-crdb-region.ts +1 -0
- package/src/describe-resources.ts +4 -1
- package/src/lifecycle-integration.test.ts +28 -0
- package/src/lint/post-synth/post-synth.test.ts +2 -3
- package/src/lint/post-synth/wgc101.test.ts +2 -3
- package/src/lint/post-synth/wgc102.test.ts +2 -3
- package/src/lint/post-synth/wgc103.test.ts +2 -3
- package/src/lint/post-synth/wgc104.test.ts +2 -3
- package/src/lint/post-synth/wgc105.test.ts +2 -3
- package/src/lint/post-synth/wgc106.test.ts +2 -3
- package/src/lint/post-synth/wgc107.test.ts +2 -3
- package/src/lint/post-synth/wgc108.test.ts +2 -3
- package/src/lint/post-synth/wgc109.test.ts +2 -3
- package/src/lint/post-synth/wgc110.test.ts +2 -3
- package/src/lint/post-synth/wgc111.test.ts +2 -3
- package/src/lint/post-synth/wgc112.test.ts +2 -3
- package/src/lint/post-synth/wgc113.test.ts +2 -3
- package/src/lint/post-synth/wgc201.test.ts +2 -3
- package/src/lint/post-synth/wgc202.test.ts +2 -3
- package/src/lint/post-synth/wgc203.test.ts +2 -3
- package/src/lint/post-synth/wgc204.test.ts +2 -3
- package/src/lint/post-synth/wgc301.test.ts +2 -3
- package/src/lint/post-synth/wgc302.test.ts +2 -3
- package/src/lint/post-synth/wgc303.test.ts +2 -3
- package/src/lint/post-synth/wgc401.test.ts +2 -3
- package/src/lint/post-synth/wgc402.test.ts +2 -3
- package/src/lint/post-synth/wgc403.test.ts +2 -3
- package/src/op/activities/gcp-apply.test.ts +39 -0
- package/src/op/activities/gcp-apply.ts +20 -3
- package/src/op/activities/index.ts +1 -0
- package/src/serializer.test.ts +3 -3
- package/src/skills/chant-gcp-patterns.md +1 -0
- 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("
|
|
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
|
-
"
|
|
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.
|
|
15
|
-
projectId
|
|
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
|
|
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",
|
|
@@ -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`).
|
|
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: (
|
|
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
|
|
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);
|
package/src/serializer.test.ts
CHANGED
|
@@ -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]);
|