@intentius/chant-lexicon-gcp 0.27.0 → 0.28.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.
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* GCP::Compute::Subnetwork → kubectl get computesubnetwork.compute.cnrm.cloud.google.com
|
|
11
11
|
*
|
|
12
12
|
* Resource-not-found is silent — `state diff --live` reports it as missing.
|
|
13
|
+
*
|
|
14
|
+
* Since this reads Config Connector CRDs through the same kubectl path as
|
|
15
|
+
* the K8s lexicon, it resolves the same environment→cluster binding (chant
|
|
16
|
+
* #1100) via `resolveClusterTarget` — `k8s.profiles.<env>.context` in
|
|
17
|
+
* `chant.config.ts` (see `lexicons/k8s/src/config.ts`), not a separate
|
|
18
|
+
* `gcp.profiles` key, because it is fundamentally the same kubectl context a
|
|
19
|
+
* project's K8s entities would use against the same cluster.
|
|
13
20
|
*/
|
|
14
21
|
import type { ResourceMetadata } from "@intentius/chant/lexicon";
|
|
15
22
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAsBjE;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASpF;AA4BD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAgD5C"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "e3947f44bb23120c8a83b0ab90621adf262b9c578b763f025673669f11112b2a",
|
|
5
5
|
"meta.json": "475f10909854d059a2e2d4fb541c058803422e38b9c64bd4d6858c0623eedb8c",
|
|
6
6
|
"types/index.d.ts": "5433b54c04b57d6fda8f3d81ef92ed0aca87f7e8e5dd3ea92d8de6e62a3ccfab",
|
|
7
7
|
"rules/hardcoded-project.ts": "228631d3159e1ffcce2359c66073d4ae59bb0285f378e46e446f416aec50481c",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"skills/chant-gcp-patterns.md": "a7ef31c1eb2f7244d3f73952c300472ef94c1eb09bd7a1003281b89299b6b704",
|
|
38
38
|
"skills/chant-gcp-gke.md": "be277019da9a722c851e47cd2dfb9c9536668948c3535fb20db7697e934c4e2b"
|
|
39
39
|
},
|
|
40
|
-
"composite": "
|
|
40
|
+
"composite": "7387aef29444d3c55659eaab15b350dda09923008bc2b95b2131b4bc956b7772"
|
|
41
41
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@intentius/chant": "^0.
|
|
79
|
+
"@intentius/chant": "^0.28.0",
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -11,6 +11,11 @@ vi.mock("node:child_process", async () => {
|
|
|
11
11
|
} };
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
const loadChantConfigMock = vi.fn();
|
|
15
|
+
vi.mock("@intentius/chant/config", () => ({
|
|
16
|
+
loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
|
|
17
|
+
}));
|
|
18
|
+
|
|
14
19
|
const { describeResources } = await import("./describe-resources");
|
|
15
20
|
|
|
16
21
|
function makeEntities(records: Array<{ name: string; entityType: string; props: Record<string, unknown> }>) {
|
|
@@ -20,6 +25,10 @@ function makeEntities(records: Array<{ name: string; entityType: string; props:
|
|
|
20
25
|
describe("gcp describeResources (Config Connector)", () => {
|
|
21
26
|
beforeEach(() => {
|
|
22
27
|
execMock.mockReset();
|
|
28
|
+
loadChantConfigMock.mockReset();
|
|
29
|
+
// No binding declared by default — matches every test below except the
|
|
30
|
+
// dedicated cluster-binding tests, which override this per case.
|
|
31
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
23
32
|
});
|
|
24
33
|
|
|
25
34
|
test("queries kubectl with the derived CC GVK and maps the response", async () => {
|
|
@@ -144,4 +153,71 @@ describe("gcp describeResources (Config Connector)", () => {
|
|
|
144
153
|
expect(result).toEqual({});
|
|
145
154
|
expect(execMock).not.toHaveBeenCalled();
|
|
146
155
|
});
|
|
156
|
+
|
|
157
|
+
// chant #1100 — GCP-via-CNRM resolves the same environment→cluster binding
|
|
158
|
+
// as the K8s lexicon (bound-and-matching, bound-and-mismatched loud
|
|
159
|
+
// refusal, unbound unchanged), since it observes through the same kubectl
|
|
160
|
+
// path against the same cluster.
|
|
161
|
+
describe("cluster binding (chant #1100)", () => {
|
|
162
|
+
function bucketEntities() {
|
|
163
|
+
return makeEntities([
|
|
164
|
+
{ name: "dataBucket", entityType: "GCP::Storage::Bucket", props: { metadata: { name: "data-bucket" } } },
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const bucketStdout = JSON.stringify({
|
|
169
|
+
metadata: { name: "data-bucket", uid: "uid-1", creationTimestamp: "2026-05-01T00:00:00Z" },
|
|
170
|
+
status: { conditions: [{ type: "Ready", status: "True" }] },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("bound and ambient context matches: observes explicitly via --context", async () => {
|
|
174
|
+
loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-cnrm" } } } } });
|
|
175
|
+
let receivedCmd = "";
|
|
176
|
+
execMock.mockImplementation((cmd: string) => {
|
|
177
|
+
if (cmd.includes("current-context")) return { stdout: "prod-cnrm\n", stderr: "" };
|
|
178
|
+
receivedCmd = cmd;
|
|
179
|
+
return { stdout: bucketStdout, stderr: "" };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() });
|
|
183
|
+
|
|
184
|
+
expect(receivedCmd).toContain("--context prod-cnrm");
|
|
185
|
+
expect(result["dataBucket"]).toMatchObject({ type: "GCP::Storage::Bucket", physicalId: "uid-1", status: "READY" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("bound and ambient context mismatches: refuses loudly instead of observing the wrong cluster", async () => {
|
|
189
|
+
loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-cnrm" } } } } });
|
|
190
|
+
execMock.mockImplementation((cmd: string) => {
|
|
191
|
+
if (cmd.includes("current-context")) return { stdout: "staging-cnrm\n", stderr: "" };
|
|
192
|
+
throw new Error(`unexpected cmd (should have refused before any kubectl get): ${cmd}`);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() }),
|
|
197
|
+
).rejects.toThrow(/environment "prod".*"prod-cnrm".*"staging-cnrm"/s);
|
|
198
|
+
|
|
199
|
+
expect(execMock).toHaveBeenCalledTimes(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("unbound: ambient context is used unchanged, but the fallback is visible (not silent)", async () => {
|
|
203
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
204
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
205
|
+
let receivedCmd = "";
|
|
206
|
+
execMock.mockImplementation((cmd: string) => {
|
|
207
|
+
receivedCmd = cmd;
|
|
208
|
+
return { stdout: bucketStdout, stderr: "" };
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() });
|
|
212
|
+
|
|
213
|
+
expect(receivedCmd).not.toContain("--context");
|
|
214
|
+
expect(receivedCmd).not.toContain("current-context");
|
|
215
|
+
expect(result["dataBucket"]).toMatchObject({ type: "GCP::Storage::Bucket", physicalId: "uid-1", status: "READY" });
|
|
216
|
+
|
|
217
|
+
const bindingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("no cluster binding"));
|
|
218
|
+
expect(bindingWarning?.[0]).toContain('environment "prod"');
|
|
219
|
+
expect(bindingWarning?.[0]).toContain("k8s.profiles.prod.context");
|
|
220
|
+
warnSpy.mockRestore();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
147
223
|
});
|
|
@@ -10,12 +10,21 @@
|
|
|
10
10
|
* GCP::Compute::Subnetwork → kubectl get computesubnetwork.compute.cnrm.cloud.google.com
|
|
11
11
|
*
|
|
12
12
|
* Resource-not-found is silent — `state diff --live` reports it as missing.
|
|
13
|
+
*
|
|
14
|
+
* Since this reads Config Connector CRDs through the same kubectl path as
|
|
15
|
+
* the K8s lexicon, it resolves the same environment→cluster binding (chant
|
|
16
|
+
* #1100) via `resolveClusterTarget` — `k8s.profiles.<env>.context` in
|
|
17
|
+
* `chant.config.ts` (see `lexicons/k8s/src/config.ts`), not a separate
|
|
18
|
+
* `gcp.profiles` key, because it is fundamentally the same kubectl context a
|
|
19
|
+
* project's K8s entities would use against the same cluster.
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
22
|
import { exec } from "node:child_process";
|
|
16
23
|
import { promisify } from "node:util";
|
|
17
24
|
import type { ResourceMetadata } from "@intentius/chant/lexicon";
|
|
18
25
|
import { hasOwnershipMarker, classifyOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
26
|
+
import { loadChantConfig } from "@intentius/chant/config";
|
|
27
|
+
import { resolveClusterTarget } from "@intentius/chant/kubectl-context";
|
|
19
28
|
|
|
20
29
|
const execAsync = promisify(exec);
|
|
21
30
|
|
|
@@ -85,6 +94,14 @@ export async function describeResources(options: {
|
|
|
85
94
|
}): Promise<Record<string, ResourceMetadata>> {
|
|
86
95
|
const result: Record<string, ResourceMetadata> = {};
|
|
87
96
|
|
|
97
|
+
// Resolve the cluster identity for this environment before touching any
|
|
98
|
+
// resource — a declared-but-mismatched binding throws here, aborting the
|
|
99
|
+
// whole describe rather than letting the per-entity try/catch below
|
|
100
|
+
// absorb it as an ordinary "not found".
|
|
101
|
+
const { config } = await loadChantConfig(process.cwd());
|
|
102
|
+
const target = await resolveClusterTarget(config as Record<string, unknown>, options.environment, "gcp");
|
|
103
|
+
const ctxArg = target.context ? ["--context", target.context] : [];
|
|
104
|
+
|
|
88
105
|
for (const [entityName, { entityType, props }] of options.entities) {
|
|
89
106
|
const gvk = deriveGVK(entityType);
|
|
90
107
|
if (!gvk) continue;
|
|
@@ -95,7 +112,7 @@ export async function describeResources(options: {
|
|
|
95
112
|
|
|
96
113
|
const kubectlResource = `${gvk.kind.toLowerCase()}.${gvk.group}`;
|
|
97
114
|
const nsArg = metadata.namespace ? ["-n", metadata.namespace] : [];
|
|
98
|
-
const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, "-o", "json"].join(" ");
|
|
115
|
+
const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, ...ctxArg, "-o", "json"].join(" ");
|
|
99
116
|
|
|
100
117
|
try {
|
|
101
118
|
const { stdout } = await execAsync(cmd);
|