@intentius/chant-lexicon-gcp 0.27.0 → 0.29.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.
@@ -9,9 +9,19 @@
9
9
  * GCP::Storage::Bucket → kubectl get storagebucket.storage.cnrm.cloud.google.com
10
10
  * GCP::Compute::Subnetwork → kubectl get computesubnetwork.compute.cnrm.cloud.google.com
11
11
  *
12
- * Resource-not-found is silent — `state diff --live` reports it as missing.
12
+ * Resource-not-found is an absence — `state diff --live` reports it as missing.
13
+ * Everything else the kubectl call can fail with (auth, an unreachable API
14
+ * server, an entity type with no derivable GVK) is NOT-OBSERVED (#1089), so a
15
+ * read that never happened cannot become a proposed `create`.
16
+ *
17
+ * Since this reads Config Connector CRDs through the same kubectl path as
18
+ * the K8s lexicon, it resolves the same environment→cluster binding (chant
19
+ * #1100) via `resolveClusterTarget` — `k8s.profiles.<env>.context` in
20
+ * `chant.config.ts` (see `lexicons/k8s/src/config.ts`), not a separate
21
+ * `gcp.profiles` key, because it is fundamentally the same kubectl context a
22
+ * project's K8s entities would use against the same cluster.
13
23
  */
14
- import type { ResourceMetadata } from "@intentius/chant/lexicon";
24
+ import type { ObservationResult } from "@intentius/chant/lexicon";
15
25
  /**
16
26
  * Mirror of `lexicons/gcp/src/serializer.ts:deriveGVKFromType` — keeping the
17
27
  * derivation logic local so describeResources can compute the kubectl resource
@@ -30,5 +40,5 @@ export declare function describeResources(options: {
30
40
  props: Record<string, unknown>;
31
41
  }>;
32
42
  owned?: boolean;
33
- }): Promise<Record<string, ResourceMetadata>>;
43
+ }): Promise<ObservationResult>;
34
44
  //# sourceMappingURL=describe-resources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAoBjE;;;;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,CAwC5C"}
1
+ {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAuBtG;;;;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,iBAAiB,CAAC,CA8E7B"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "0fcdcb7ff056aa43420690a59a0927d5cfb50ab6de1b3399a399dbb529ea8782",
4
+ "manifest.json": "fa47fcfe6d8f3de4d1844dce0ccf6c7d35bfb0596dd700a11ea870986b29e8e6",
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": "3ae099bad7752191fc7ffaec95374370d69de052349b8888efeafc758aeca632"
40
+ "composite": "83f8a5fc48ff3282c804d14768e9c904873d2fdc1b168276400780da729a4ba8"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcp",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GCP",
6
6
  "intrinsics": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.27.0",
3
+ "version": "0.29.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.27.0",
79
+ "@intentius/chant": "^0.29.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 () => {
@@ -46,7 +55,7 @@ describe("gcp describeResources (Config Connector)", () => {
46
55
  expect(receivedCmd).toContain("data-bucket");
47
56
  expect(receivedCmd).toContain("-n config-control");
48
57
 
49
- expect(result["dataBucket"]).toMatchObject({
58
+ expect(result.resources["dataBucket"]).toMatchObject({
50
59
  type: "GCP::Storage::Bucket",
51
60
  physicalId: "uid-1",
52
61
  status: "READY",
@@ -90,7 +99,7 @@ describe("gcp describeResources (Config Connector)", () => {
90
99
 
91
100
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["x"], entities });
92
101
 
93
- expect(result["x"].status).toBe("DependencyNotFound");
102
+ expect(result.resources["x"].status).toBe("DependencyNotFound");
94
103
  });
95
104
 
96
105
  test("missing Ready condition falls back to PRESENT", async () => {
@@ -108,10 +117,10 @@ describe("gcp describeResources (Config Connector)", () => {
108
117
 
109
118
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["x"], entities });
110
119
 
111
- expect(result["x"].status).toBe("PRESENT");
120
+ expect(result.resources["x"].status).toBe("PRESENT");
112
121
  });
113
122
 
114
- test("kubectl-not-found leaves entity out of result", async () => {
123
+ test("kubectl-not-found leaves entity out of result (a confirmed absence)", async () => {
115
124
  execMock.mockImplementation(() => { throw new Error('Error from server (NotFound): storagebucket "x" not found'); });
116
125
 
117
126
  const entities = makeEntities([
@@ -120,28 +129,115 @@ describe("gcp describeResources (Config Connector)", () => {
120
129
 
121
130
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["x"], entities });
122
131
 
123
- expect(result).toEqual({});
132
+ expect(result.resources).toEqual({});
133
+ expect(result.unobserved ?? {}).toEqual({});
134
+ });
135
+
136
+ test("an unreachable cluster is unobserved, not absent (#1089)", async () => {
137
+ execMock.mockImplementation(() => {
138
+ throw Object.assign(new Error("kubectl failed"), {
139
+ stderr: "Unable to connect to the server: dial tcp: i/o timeout",
140
+ });
141
+ });
142
+
143
+ const entities = makeEntities([
144
+ { name: "x", entityType: "GCP::Storage::Bucket", props: { metadata: { name: "x" } } },
145
+ ]);
146
+
147
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["x"], entities });
148
+
149
+ expect(result.resources).toEqual({});
150
+ expect(result.unobserved?.x?.reason).toBe("no-binding");
124
151
  });
125
152
 
126
- test("non-GCP entity types are skipped", async () => {
153
+ test("non-GCP entity types are unobserved — no GVK to query (#1089)", async () => {
127
154
  const entities = makeEntities([
128
155
  { name: "x", entityType: "AWS::S3::Bucket", props: { metadata: { name: "x" } } },
129
156
  ]);
130
157
 
131
158
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["x"], entities });
132
159
 
133
- expect(result).toEqual({});
160
+ expect(result.resources).toEqual({});
161
+ expect(result.unobserved?.x?.reason).toBe("unsupported-kind");
134
162
  expect(execMock).not.toHaveBeenCalled();
135
163
  });
136
164
 
137
- test("entity without metadata.name is silently skipped", async () => {
165
+ test("entity without metadata.name is unobserved — nothing was queried", async () => {
138
166
  const entities = makeEntities([
139
167
  { name: "broken", entityType: "GCP::Storage::Bucket", props: {} },
140
168
  ]);
141
169
 
142
170
  const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["broken"], entities });
143
171
 
144
- expect(result).toEqual({});
172
+ expect(result.resources).toEqual({});
173
+ expect(result.unobserved?.broken?.reason).toBe("read-failed");
145
174
  expect(execMock).not.toHaveBeenCalled();
146
175
  });
176
+
177
+ // chant #1100 — GCP-via-CNRM resolves the same environment→cluster binding
178
+ // as the K8s lexicon (bound-and-matching, bound-and-mismatched loud
179
+ // refusal, unbound unchanged), since it observes through the same kubectl
180
+ // path against the same cluster.
181
+ describe("cluster binding (chant #1100)", () => {
182
+ function bucketEntities() {
183
+ return makeEntities([
184
+ { name: "dataBucket", entityType: "GCP::Storage::Bucket", props: { metadata: { name: "data-bucket" } } },
185
+ ]);
186
+ }
187
+
188
+ const bucketStdout = JSON.stringify({
189
+ metadata: { name: "data-bucket", uid: "uid-1", creationTimestamp: "2026-05-01T00:00:00Z" },
190
+ status: { conditions: [{ type: "Ready", status: "True" }] },
191
+ });
192
+
193
+ test("bound and ambient context matches: observes explicitly via --context", async () => {
194
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-cnrm" } } } } });
195
+ let receivedCmd = "";
196
+ execMock.mockImplementation((cmd: string) => {
197
+ if (cmd.includes("current-context")) return { stdout: "prod-cnrm\n", stderr: "" };
198
+ receivedCmd = cmd;
199
+ return { stdout: bucketStdout, stderr: "" };
200
+ });
201
+
202
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() });
203
+
204
+ expect(receivedCmd).toContain("--context prod-cnrm");
205
+ expect(result.resources["dataBucket"]).toMatchObject({ type: "GCP::Storage::Bucket", physicalId: "uid-1", status: "READY" });
206
+ });
207
+
208
+ test("bound and ambient context mismatches: refuses loudly instead of observing the wrong cluster", async () => {
209
+ loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-cnrm" } } } } });
210
+ execMock.mockImplementation((cmd: string) => {
211
+ if (cmd.includes("current-context")) return { stdout: "staging-cnrm\n", stderr: "" };
212
+ throw new Error(`unexpected cmd (should have refused before any kubectl get): ${cmd}`);
213
+ });
214
+
215
+ await expect(
216
+ describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() }),
217
+ ).rejects.toThrow(/environment "prod".*"prod-cnrm".*"staging-cnrm"/s);
218
+
219
+ expect(execMock).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ test("unbound: ambient context is used unchanged, but the fallback is visible (not silent)", async () => {
223
+ loadChantConfigMock.mockResolvedValue({ config: {} });
224
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
225
+ let receivedCmd = "";
226
+ execMock.mockImplementation((cmd: string) => {
227
+ receivedCmd = cmd;
228
+ return { stdout: bucketStdout, stderr: "" };
229
+ });
230
+
231
+ const result = await describeResources({ environment: "prod", buildOutput: "", entityNames: ["dataBucket"], entities: bucketEntities() });
232
+
233
+ expect(receivedCmd).not.toContain("--context");
234
+ expect(receivedCmd).not.toContain("current-context");
235
+ expect(result.resources["dataBucket"]).toMatchObject({ type: "GCP::Storage::Bucket", physicalId: "uid-1", status: "READY" });
236
+
237
+ const bindingWarning = warnSpy.mock.calls.find((c) => String(c[0]).includes("no cluster binding"));
238
+ expect(bindingWarning?.[0]).toContain('environment "prod"');
239
+ expect(bindingWarning?.[0]).toContain("k8s.profiles.prod.context");
240
+ warnSpy.mockRestore();
241
+ });
242
+ });
147
243
  });
@@ -9,13 +9,26 @@
9
9
  * GCP::Storage::Bucket → kubectl get storagebucket.storage.cnrm.cloud.google.com
10
10
  * GCP::Compute::Subnetwork → kubectl get computesubnetwork.compute.cnrm.cloud.google.com
11
11
  *
12
- * Resource-not-found is silent — `state diff --live` reports it as missing.
12
+ * Resource-not-found is an absence — `state diff --live` reports it as missing.
13
+ * Everything else the kubectl call can fail with (auth, an unreachable API
14
+ * server, an entity type with no derivable GVK) is NOT-OBSERVED (#1089), so a
15
+ * read that never happened cannot become a proposed `create`.
16
+ *
17
+ * Since this reads Config Connector CRDs through the same kubectl path as
18
+ * the K8s lexicon, it resolves the same environment→cluster binding (chant
19
+ * #1100) via `resolveClusterTarget` — `k8s.profiles.<env>.context` in
20
+ * `chant.config.ts` (see `lexicons/k8s/src/config.ts`), not a separate
21
+ * `gcp.profiles` key, because it is fundamentally the same kubectl context a
22
+ * project's K8s entities would use against the same cluster.
13
23
  */
14
24
 
15
25
  import { exec } from "node:child_process";
16
26
  import { promisify } from "node:util";
17
- import type { ResourceMetadata } from "@intentius/chant/lexicon";
27
+ import type { ObservationResult, ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
28
+ import { observation } from "@intentius/chant/observation";
18
29
  import { hasOwnershipMarker, classifyOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
30
+ import { loadChantConfig } from "@intentius/chant/config";
31
+ import { resolveClusterTarget, classifyKubectlFailure } from "@intentius/chant/kubectl-context";
19
32
 
20
33
  const execAsync = promisify(exec);
21
34
 
@@ -82,26 +95,57 @@ export async function describeResources(options: {
82
95
  entityNames: string[];
83
96
  entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
84
97
  owned?: boolean;
85
- }): Promise<Record<string, ResourceMetadata>> {
98
+ }): Promise<ObservationResult> {
86
99
  const result: Record<string, ResourceMetadata> = {};
100
+ const unobserved: Record<string, UnobservedEntity> = {};
101
+
102
+ // Resolve the cluster identity for this environment before touching any
103
+ // resource — a declared-but-mismatched binding throws here, aborting the
104
+ // whole describe rather than letting the per-entity try/catch below
105
+ // absorb it as an ordinary "not found".
106
+ const { config } = await loadChantConfig(process.cwd());
107
+ const target = await resolveClusterTarget(config as Record<string, unknown>, options.environment, "gcp");
108
+ const ctxArg = target.context ? ["--context", target.context] : [];
87
109
 
88
110
  for (const [entityName, { entityType, props }] of options.entities) {
89
111
  const gvk = deriveGVK(entityType);
90
- if (!gvk) continue;
112
+ if (!gvk) {
113
+ // Not a `GCP::Service::Kind` this lexicon can turn into a Config
114
+ // Connector GVK — nothing was queried, so nothing is known (#1089).
115
+ unobserved[entityName] = {
116
+ type: entityType,
117
+ reason: "unsupported-kind",
118
+ detail: `cannot derive a Config Connector GVK from ${entityType}`,
119
+ };
120
+ continue;
121
+ }
91
122
 
92
123
  const metadata = props.metadata as { name?: string; namespace?: string } | undefined;
93
124
  const name = metadata?.name;
94
- if (!name) continue;
125
+ if (!name) {
126
+ unobserved[entityName] = {
127
+ type: entityType,
128
+ reason: "read-failed",
129
+ detail: "declared entity has no metadata.name to query by",
130
+ };
131
+ continue;
132
+ }
95
133
 
96
134
  const kubectlResource = `${gvk.kind.toLowerCase()}.${gvk.group}`;
97
135
  const nsArg = metadata.namespace ? ["-n", metadata.namespace] : [];
98
- const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, "-o", "json"].join(" ");
136
+ const cmd = ["kubectl", "get", kubectlResource, name, ...nsArg, ...ctxArg, "-o", "json"].join(" ");
99
137
 
100
138
  try {
101
139
  const { stdout } = await execAsync(cmd);
102
140
  const obj: KubectlResponse = JSON.parse(stdout);
103
- // owned filter: skip resources not carrying chant's marker label.
141
+ // owned filter: withhold resources not carrying chant's marker label.
142
+ // Withheld is not absent (#1089) — the CR exists, it just isn't chant's.
104
143
  if (options.owned && !hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS)) {
144
+ unobserved[entityName] = {
145
+ type: entityType,
146
+ reason: "filtered",
147
+ detail: "live resource carries no chant ownership marker and --owned was requested",
148
+ };
105
149
  continue;
106
150
  }
107
151
  result[entityName] = {
@@ -116,10 +160,17 @@ export async function describeResources(options: {
116
160
  annotations: obj.metadata?.annotations,
117
161
  }),
118
162
  };
119
- } catch {
120
- // CRD or instance not found skip silently
163
+ } catch (err) {
164
+ // A NotFound is a real absence (the CR isn't there, or Config Connector
165
+ // doesn't serve that CRD, so no instance can be). Anything else — auth,
166
+ // an unreachable API server, a context that doesn't resolve — proves
167
+ // nothing and is reported as a hole rather than an absence (#1089).
168
+ const outcome = classifyKubectlFailure(err);
169
+ if (outcome.kind === "unobserved") {
170
+ unobserved[entityName] = { type: entityType, reason: outcome.reason, detail: outcome.detail };
171
+ }
121
172
  }
122
173
  }
123
174
 
124
- return result;
175
+ return observation(result, unobserved);
125
176
  }
@@ -31,6 +31,8 @@ vi.mock("node:child_process", async () => {
31
31
  const { gcpPlugin } = await import("./plugin");
32
32
  const { liveImportFromPlugins } = await import("@intentius/chant/cli/commands/import");
33
33
  const { buildChangeSet } = await import("@intentius/chant/lifecycle/change-set");
34
+ const { normalizeObservation } = await import("@intentius/chant/observation");
35
+ const { describeObservationConformance } = await import("@intentius/chant-test-utils");
34
36
 
35
37
  const liveBucket = {
36
38
  apiVersion: "storage.cnrm.cloud.google.com/v1beta1",
@@ -84,20 +86,22 @@ describe("gcp lifecycle integration (#163)", () => {
84
86
  : new Error("not found"),
85
87
  );
86
88
 
87
- const observedNow = await gcpPlugin.describeResources!({
88
- environment: "prod",
89
- buildOutput: "",
90
- entityNames: ["dataBucket"],
91
- entities: new Map([
92
- [
93
- "dataBucket",
94
- {
95
- entityType: "GCP::Storage::Bucket",
96
- props: { metadata: { name: "data-bucket", namespace: "config-control" } },
97
- },
98
- ],
99
- ]),
100
- });
89
+ const { resources: observedNow } = normalizeObservation(
90
+ await gcpPlugin.describeResources!({
91
+ environment: "prod",
92
+ buildOutput: "",
93
+ entityNames: ["dataBucket"],
94
+ entities: new Map([
95
+ [
96
+ "dataBucket",
97
+ {
98
+ entityType: "GCP::Storage::Bucket",
99
+ props: { metadata: { name: "data-bucket", namespace: "config-control" } },
100
+ },
101
+ ],
102
+ ]),
103
+ }),
104
+ );
101
105
  expect(observedNow.dataBucket?.type).toBe("GCP::Storage::Bucket");
102
106
 
103
107
  const cs = buildChangeSet("prod", {
@@ -117,3 +121,46 @@ describe("gcp lifecycle integration (#163)", () => {
117
121
  expect(cs2.entries.find((e) => e.name === "dataBucket")!.action).toBe("noop");
118
122
  });
119
123
  });
124
+
125
+ // The shared conformance suite (#1089).
126
+ describeObservationConformance({
127
+ lexicon: "gcp",
128
+ scenarios: [
129
+ {
130
+ name: "an entity type with no derivable Config Connector GVK",
131
+ declared: ["notGcp", "gone"],
132
+ expectUnobserved: ["notGcp"],
133
+ expectAbsent: ["gone"],
134
+ run: () => {
135
+ execMock.mockImplementation(() => new Error('Error from server (NotFound): storagebucket "gone" not found'));
136
+ return gcpPlugin.describeResources!({
137
+ environment: "prod",
138
+ buildOutput: "",
139
+ entityNames: ["notGcp", "gone"],
140
+ entities: new Map([
141
+ ["notGcp", { entityType: "AWS::S3::Bucket", props: { metadata: { name: "not-gcp" } } }],
142
+ ["gone", { entityType: "GCP::Storage::Bucket", props: { metadata: { name: "gone" } } }],
143
+ ]),
144
+ });
145
+ },
146
+ },
147
+ {
148
+ name: "an unreachable Config Connector cluster",
149
+ declared: ["dataBucket"],
150
+ expectUnobserved: ["dataBucket"],
151
+ run: () => {
152
+ execMock.mockImplementation(() =>
153
+ Object.assign(new Error("kubectl failed"), { stderr: "Unable to connect to the server: dial tcp: i/o timeout" }),
154
+ );
155
+ return gcpPlugin.describeResources!({
156
+ environment: "prod",
157
+ buildOutput: "",
158
+ entityNames: ["dataBucket"],
159
+ entities: new Map([
160
+ ["dataBucket", { entityType: "GCP::Storage::Bucket", props: { metadata: { name: "data-bucket" } } }],
161
+ ]),
162
+ });
163
+ },
164
+ },
165
+ ],
166
+ });