@caelo-cms/provisioning 0.10.69 → 0.10.71

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 (56) hide show
  1. package/dist/adopt-existing.d.ts +119 -0
  2. package/dist/adopt-existing.d.ts.map +1 -0
  3. package/dist/adopt-existing.js +118 -0
  4. package/dist/adopt-existing.js.map +1 -0
  5. package/dist/preview-plan.d.ts +7 -0
  6. package/dist/preview-plan.d.ts.map +1 -1
  7. package/dist/preview-plan.js +1 -0
  8. package/dist/preview-plan.js.map +1 -1
  9. package/dist/remote-mcp.d.ts +27 -8
  10. package/dist/remote-mcp.d.ts.map +1 -1
  11. package/dist/remote-mcp.js +88 -56
  12. package/dist/remote-mcp.js.map +1 -1
  13. package/dist/stack-contract.d.ts +53 -39
  14. package/dist/stack-contract.d.ts.map +1 -1
  15. package/dist/stack-contract.js +110 -0
  16. package/dist/stack-contract.js.map +1 -1
  17. package/dist/state-command.d.ts.map +1 -1
  18. package/dist/state-command.js +27 -1
  19. package/dist/state-command.js.map +1 -1
  20. package/dist/state-migrate.d.ts +7 -0
  21. package/dist/state-migrate.d.ts.map +1 -1
  22. package/dist/state-migrate.js +18 -1
  23. package/dist/state-migrate.js.map +1 -1
  24. package/dist/state-pulumi.d.ts +7 -0
  25. package/dist/state-pulumi.d.ts.map +1 -1
  26. package/dist/state-pulumi.js +11 -0
  27. package/dist/state-pulumi.js.map +1 -1
  28. package/dist/upgrade/converge.d.ts.map +1 -1
  29. package/dist/upgrade/converge.js +4 -2
  30. package/dist/upgrade/converge.js.map +1 -1
  31. package/dist/wizards/gcp-pulumi.d.ts +28 -5
  32. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  33. package/dist/wizards/gcp-pulumi.js +70 -8
  34. package/dist/wizards/gcp-pulumi.js.map +1 -1
  35. package/dist/wizards/gcp.d.ts.map +1 -1
  36. package/dist/wizards/gcp.js +100 -46
  37. package/dist/wizards/gcp.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/__tests__/fixtures/viu-one-plan-state-migrate.txt +98 -0
  40. package/src/adopt-existing.test.ts +397 -0
  41. package/src/adopt-existing.ts +228 -0
  42. package/src/preview-plan.ts +8 -0
  43. package/src/remote-mcp-stack-config.test.ts +195 -0
  44. package/src/remote-mcp.test.ts +99 -16
  45. package/src/remote-mcp.ts +104 -58
  46. package/src/stack-contract-parity.test.ts +28 -3
  47. package/src/stack-contract.ts +107 -6
  48. package/src/state-command.ts +38 -1
  49. package/src/state-migrate.test.ts +38 -0
  50. package/src/state-migrate.ts +24 -1
  51. package/src/state-pulumi.ts +21 -0
  52. package/src/upgrade/converge.ts +4 -2
  53. package/src/wizards/gcp-pulumi.ts +105 -9
  54. package/src/wizards/gcp.ts +126 -55
  55. package/stacks/gcp/index.ts +13 -2
  56. package/stacks/gcp-firebase/index.ts +20 -6
@@ -0,0 +1,98 @@
1
+ Pulumi preview (state-migrate), 2026-10-24T14:31:26.306Z: 37 change(s)
2
+
3
+ DELETES (8):
4
+ gcp:secretmanager/secretVersion:SecretVersion caelo-production-cookie-secret-v1 <-- LOSES DATA: Secret Manager secret value
5
+ gcp:secretmanager/secretVersion:SecretVersion caelo-production-csrf-secret-v1 <-- LOSES DATA: Secret Manager secret value
6
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-csrf-secret-binding
7
+ random:index/randomBytes:RandomBytes caelo-production-cookie-secret-bytes
8
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-cookie-secret-binding
9
+ random:index/randomBytes:RandomBytes caelo-production-csrf-secret-bytes
10
+ gcp:secretmanager/secret:Secret caelo-production-cookie-secret <-- LOSES DATA: Secret Manager secret
11
+ gcp:secretmanager/secret:Secret caelo-production-csrf-secret <-- LOSES DATA: Secret Manager secret
12
+ Updates in place (15):
13
+ gcp:compute/network:Network caelo-production-vpc (deletionPolicy)
14
+ gcp:storage/bucket:Bucket caelo-production-media (deletionPolicy)
15
+ gcp:secretmanager/secret:Secret caelo-production-secret-kek (deletionPolicy)
16
+ gcp:secretmanager/secret:Secret caelo-production-postgres-password (deletionPolicy)
17
+ gcp:serviceaccount/account:Account caelo-production-run-sa (deletionPolicy)
18
+ gcp:compute/subnetwork:Subnetwork caelo-production-subnet (deletionPolicy)
19
+ gcp:compute/globalAddress:GlobalAddress caelo-production-pg-private-ip (deletionPolicy)
20
+ gcp:firebase/hostingSite:HostingSite caelo-production-firebase-site (deletionPolicy)
21
+ gcp:servicenetworking/connection:Connection caelo-production-pg-peering (deletionPolicy)
22
+ gcp:firebase/hostingCustomDomain:HostingCustomDomain caelo-production-firebase-apex (deletionPolicy)
23
+ gcp:sql/databaseInstance:DatabaseInstance caelo-production-pg (deletionPolicy, settings)
24
+ gcp:sql/user:User caelo-production-admin-user (deletionPolicy)
25
+ gcp:cloudrunv2/service:Service caelo-production-gateway (client, clientVersion, deletionPolicy, template)
26
+ gcp:cloudrunv2/service:Service caelo-production-admin (client, clientVersion, deletionPolicy, template)
27
+ gcp:sql/user:User caelo-production-public-user (deletionPolicy, password)
28
+ Creates (14):
29
+ gcp:serviceaccount/account:Account caelo-production-gateway-sa
30
+ gcp:serviceaccount/account:Account caelo-production-mcp-sa
31
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-gateway-mcp-oauth-seal-key-binding
32
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-gateway-staging-gate-secret-binding
33
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-gateway-operator-oauth-client-binding
34
+ gcp:projects/iAMMember:IAMMember caelo-production-gateway-log-writer
35
+ gcp:projects/iAMMember:IAMMember caelo-production-gateway-metric-writer
36
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-gateway-public-role-password-binding
37
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-staging-gate-secret-binding
38
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-admin-operator-oauth-client-version-adder
39
+ gcp:serviceaccount/iAMMember:IAMMember caelo-production-mcp-token-creator-user-owner-example-com
40
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-tool-approval-secret-binding
41
+ gcp:secretmanager/secretIamMember:SecretIamMember caelo-production-internal-secret-binding
42
+ gcp:iap/webCloudRunServiceIamMember:WebCloudRunServiceIamMember caelo-production-admin-iap-mcp
43
+
44
+ 4 of these would destroy data: Secret Manager secret value caelo-production-cookie-secret-v1; Secret Manager secret value caelo-production-csrf-secret-v1; Secret Manager secret caelo-production-cookie-secret; Secret Manager secret caelo-production-csrf-secret.
45
+
46
+ Every step:
47
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:compute/network:Network::caelo-production-vpc
48
+ changes: deletionPolicy
49
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:storage/bucket:Bucket::caelo-production-media
50
+ changes: deletionPolicy
51
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secret:Secret::caelo-production-secret-kek
52
+ changes: deletionPolicy
53
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secret:Secret::caelo-production-postgres-password
54
+ changes: deletionPolicy
55
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:serviceaccount/account:Account::caelo-production-gateway-sa
56
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:serviceaccount/account:Account::caelo-production-run-sa
57
+ changes: deletionPolicy
58
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:serviceaccount/account:Account::caelo-production-mcp-sa
59
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:compute/subnetwork:Subnetwork::caelo-production-subnet
60
+ changes: deletionPolicy
61
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:compute/globalAddress:GlobalAddress::caelo-production-pg-private-ip
62
+ changes: deletionPolicy
63
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:firebase/hostingSite:HostingSite::caelo-production-firebase-site
64
+ changes: deletionPolicy
65
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-gateway-mcp-oauth-seal-key-binding
66
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-gateway-staging-gate-secret-binding
67
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-gateway-operator-oauth-client-binding
68
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:projects/iAMMember:IAMMember::caelo-production-gateway-log-writer
69
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:projects/iAMMember:IAMMember::caelo-production-gateway-metric-writer
70
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-gateway-public-role-password-binding
71
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-staging-gate-secret-binding
72
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-admin-operator-oauth-client-version-adder
73
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:serviceaccount/iAMMember:IAMMember::caelo-production-mcp-token-creator-user-owner-example-com
74
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-tool-approval-secret-binding
75
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-internal-secret-binding
76
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:servicenetworking/connection:Connection::caelo-production-pg-peering
77
+ changes: deletionPolicy
78
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:firebase/hostingCustomDomain:HostingCustomDomain::caelo-production-firebase-apex
79
+ changes: deletionPolicy
80
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:sql/databaseInstance:DatabaseInstance::caelo-production-pg
81
+ changes: deletionPolicy, settings
82
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:sql/user:User::caelo-production-admin-user
83
+ changes: deletionPolicy
84
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:cloudrunv2/service:Service::caelo-production-gateway
85
+ changes: client, clientVersion, deletionPolicy, template
86
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:cloudrunv2/service:Service::caelo-production-admin
87
+ changes: client, clientVersion, deletionPolicy, template
88
+ - create urn:pulumi:production::caelo-gcp-firebase::gcp:iap/webCloudRunServiceIamMember:WebCloudRunServiceIamMember::caelo-production-admin-iap-mcp
89
+ - update urn:pulumi:production::caelo-gcp-firebase::gcp:sql/user:User::caelo-production-public-user
90
+ changes: deletionPolicy, password
91
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretVersion:SecretVersion::caelo-production-cookie-secret-v1
92
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretVersion:SecretVersion::caelo-production-csrf-secret-v1
93
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-csrf-secret-binding
94
+ - delete urn:pulumi:production::caelo-gcp-firebase::random:index/randomBytes:RandomBytes::caelo-production-cookie-secret-bytes
95
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secretIamMember:SecretIamMember::caelo-production-cookie-secret-binding
96
+ - delete urn:pulumi:production::caelo-gcp-firebase::random:index/randomBytes:RandomBytes::caelo-production-csrf-secret-bytes
97
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secret:Secret::caelo-production-cookie-secret
98
+ - delete urn:pulumi:production::caelo-gcp-firebase::gcp:secretmanager/secret:Secret::caelo-production-csrf-secret
@@ -0,0 +1,397 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * adopt-existing.ts (#1079) against fake gcloud and Pulumi runners, on the
5
+ * plan viu.one's installer re-run produced: a stack converged by `upgrade`
6
+ * through gcloud plans to create service accounts that exist already. They
7
+ * are imported, the re-plan no longer creates them; anything that doesn't
8
+ * exist, any other kind and any failure is handled as the file comment says.
9
+ */
10
+
11
+ import { describe, expect, it } from "bun:test";
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import type { EngineEvent } from "@pulumi/pulumi/automation";
15
+ import {
16
+ type AdoptionCandidate,
17
+ AdoptionError,
18
+ adoptExistingResources,
19
+ adoptionCandidates,
20
+ formatAdopted,
21
+ } from "./adopt-existing.js";
22
+ import type { GcloudResult } from "./gcloud.js";
23
+ import type { GcloudRunner } from "./gcloud-retry.js";
24
+ import { gatewayServiceAccountId, MCP_IAP_SERVICE_ACCOUNT_ID } from "./gcp-names.js";
25
+ import { collectPreviewSteps, type PlannedOp, type PlannedStep } from "./preview-plan.js";
26
+ import { stackGcpProviderUrn } from "./wizards/gcp-pulumi.js";
27
+
28
+ const PROJECT = "viu-one-web";
29
+ const SA = "gcp:serviceaccount/account:Account";
30
+ const PROVIDER_URN =
31
+ "urn:pulumi:production::caelo-gcp-firebase::pulumi:providers:gcp::caelo-production-gcp";
32
+
33
+ /**
34
+ * The inputs the gcp-firebase stack declares for the creates in the fixture
35
+ * (the plan file lists names only): the two service accounts as
36
+ * stacks/gcp-firebase/index.ts declares them, IAM members with a role.
37
+ */
38
+ function declaredInputs(type: string, name: string): Record<string, unknown> {
39
+ if (name === "caelo-production-gateway-sa") {
40
+ return {
41
+ accountId: gatewayServiceAccountId("production"),
42
+ displayName: "Caelo production gateway",
43
+ createIgnoreAlreadyExists: true,
44
+ };
45
+ }
46
+ if (name === "caelo-production-mcp-sa") {
47
+ return {
48
+ project: PROJECT,
49
+ accountId: MCP_IAP_SERVICE_ACCOUNT_ID,
50
+ displayName: "Caelo MCP (IAP ingress)",
51
+ createIgnoreAlreadyExists: true,
52
+ };
53
+ }
54
+ return { role: "roles/secretmanager.secretAccessor", type };
55
+ }
56
+
57
+ /**
58
+ * The "Every step:" section of the real viu.one plan (preview-plan.ts
59
+ * writePlanFile, 2026-10-24; the Owner's email in one resource name replaced)
60
+ * as the planned steps the preview produced.
61
+ */
62
+ function viuOnePlan(): PlannedStep[] {
63
+ const text = readFileSync(
64
+ join(import.meta.dir, "__tests__/fixtures/viu-one-plan-state-migrate.txt"),
65
+ "utf8",
66
+ );
67
+ const body = text.slice(text.indexOf("Every step:"));
68
+ const steps: PlannedStep[] = [];
69
+ for (const line of body.split("\n")) {
70
+ const step = /^- (\w+) (urn:\S+)$/.exec(line);
71
+ if (step) {
72
+ const urn = step[2] as string;
73
+ const [, , type, name] = urn.split("::") as [string, string, string, string];
74
+ const op = step[1] as PlannedOp;
75
+ steps.push({
76
+ op,
77
+ type,
78
+ name,
79
+ urn,
80
+ diffs: [],
81
+ replaceReasons: [],
82
+ ...(op === "create" ? { inputs: declaredInputs(type, name) } : {}),
83
+ });
84
+ continue;
85
+ }
86
+ const changes = /^ {4}changes: (.+)$/.exec(line);
87
+ const last = steps.at(-1);
88
+ if (changes && last) {
89
+ steps[steps.length - 1] = { ...last, diffs: (changes[1] as string).split(", ") };
90
+ }
91
+ }
92
+ return steps;
93
+ }
94
+
95
+ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
96
+ const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
97
+
98
+ /** gcloud with these service-account emails in the project; every call recorded. */
99
+ function fakeGcloud(existing: readonly string[], other: Record<string, string> = {}) {
100
+ const calls: string[][] = [];
101
+ const run: GcloudRunner = async (args) => {
102
+ calls.push(args);
103
+ const email = args[3] ?? "";
104
+ if (args.slice(0, 3).join(" ") !== "iam service-accounts describe") {
105
+ return fail(`unexpected gcloud ${args.join(" ")}`);
106
+ }
107
+ if (other[email]) return fail(other[email] as string);
108
+ return existing.includes(email)
109
+ ? ok(email)
110
+ : fail("ERROR: (gcloud.iam.service-accounts.describe) NOT_FOUND: Unknown service account.");
111
+ };
112
+ return { run, calls };
113
+ }
114
+
115
+ /**
116
+ * A stack whose preview is `plan`, minus the creates `pulumi import` took
117
+ * into its state (an imported account shows as an update of the
118
+ * Terraform-only `createIgnoreAlreadyExists`, which import cannot read).
119
+ */
120
+ function fakePulumi(plan: readonly PlannedStep[], opts: { importFails?: boolean } = {}) {
121
+ const imported = new Set<string>();
122
+ const imports: { type: string; name: string; id: string }[][] = [];
123
+ let previews = 0;
124
+ let ups = 0;
125
+ return {
126
+ imports,
127
+ get previews() {
128
+ return previews;
129
+ },
130
+ get ups() {
131
+ return ups;
132
+ },
133
+ up: () => {
134
+ ups++;
135
+ },
136
+ preview: async (): Promise<readonly PlannedStep[]> => {
137
+ previews++;
138
+ return plan.map((s) =>
139
+ s.op === "create" && imported.has(s.urn)
140
+ ? {
141
+ op: "update" as const,
142
+ type: s.type,
143
+ name: s.name,
144
+ urn: s.urn,
145
+ diffs: ["createIgnoreAlreadyExists"],
146
+ replaceReasons: [],
147
+ }
148
+ : s,
149
+ );
150
+ },
151
+ importResources: async (cs: readonly AdoptionCandidate[]) => {
152
+ if (opts.importFails) {
153
+ throw new Error(
154
+ "code: 255\n stderr: error: Preview failed: resource 'projects/x' does not exist",
155
+ );
156
+ }
157
+ imports.push(cs.map((c) => ({ type: c.step.type, name: c.step.name, id: c.importId })));
158
+ for (const c of cs) imported.add(c.step.urn);
159
+ },
160
+ };
161
+ }
162
+
163
+ const GATEWAY_EMAIL = `caelo-production-gateway-sa@${PROJECT}.iam.gserviceaccount.com`;
164
+ const MCP_EMAIL = `caelo-mcp@${PROJECT}.iam.gserviceaccount.com`;
165
+
166
+ /** The installer's order (wizards/gcp-pulumi.ts pulumiUpGcp): adopt, review, up. */
167
+ async function installerRun(
168
+ plan: readonly PlannedStep[],
169
+ gcloud: GcloudRunner,
170
+ pulumi: ReturnType<typeof fakePulumi>,
171
+ ) {
172
+ const result = await adoptExistingResources(plan, {
173
+ projectId: PROJECT,
174
+ gcloud,
175
+ preview: pulumi.preview,
176
+ importResources: pulumi.importResources,
177
+ });
178
+ pulumi.up();
179
+ return result;
180
+ }
181
+
182
+ describe("adoptExistingResources — viu.one's installer re-run (#1079)", () => {
183
+ it("the fixture is the real plan: 8 deletes, 15 updates, 14 creates", () => {
184
+ const plan = viuOnePlan();
185
+ const count = (op: PlannedOp) => plan.filter((s) => s.op === op).length;
186
+ expect([count("delete"), count("update"), count("create")]).toEqual([8, 15, 14]);
187
+ });
188
+
189
+ it("imports the service accounts upgrade made; the re-plan creates neither", async () => {
190
+ const plan = viuOnePlan();
191
+ const gcloud = fakeGcloud([GATEWAY_EMAIL, MCP_EMAIL]);
192
+ const pulumi = fakePulumi(plan);
193
+ const { adopted, steps } = await installerRun(plan, gcloud.run, pulumi);
194
+
195
+ expect(pulumi.imports).toEqual([
196
+ [
197
+ {
198
+ type: SA,
199
+ name: "caelo-production-gateway-sa",
200
+ id: `projects/${PROJECT}/serviceAccounts/${GATEWAY_EMAIL}`,
201
+ },
202
+ {
203
+ type: SA,
204
+ name: "caelo-production-mcp-sa",
205
+ id: `projects/${PROJECT}/serviceAccounts/${MCP_EMAIL}`,
206
+ },
207
+ ],
208
+ ]);
209
+ expect(adopted.map((c) => c.display)).toEqual([GATEWAY_EMAIL, MCP_EMAIL]);
210
+ expect(steps.filter((s) => s.op === "create" && s.type === SA)).toEqual([]);
211
+ // The IAM members stay creates (idempotent), the retired-secret deletes stay.
212
+ expect(steps.filter((s) => s.op === "create")).toHaveLength(12);
213
+ expect(steps.filter((s) => s.op === "delete")).toHaveLength(8);
214
+ expect(pulumi.previews).toBe(1);
215
+ expect(pulumi.ups).toBe(1);
216
+ // Only the two service accounts were probed: no IAM member, no other kind.
217
+ expect(gcloud.calls.map((a) => a[3])).toEqual([GATEWAY_EMAIL, MCP_EMAIL]);
218
+ expect(gcloud.calls[0]).toEqual([
219
+ "iam",
220
+ "service-accounts",
221
+ "describe",
222
+ GATEWAY_EMAIL,
223
+ `--project=${PROJECT}`,
224
+ "--format=value(email)",
225
+ ]);
226
+ });
227
+
228
+ it("shows what it adopted", async () => {
229
+ const plan = viuOnePlan();
230
+ const { adopted } = await installerRun(plan, fakeGcloud([GATEWAY_EMAIL]).run, fakePulumi(plan));
231
+ expect(formatAdopted(adopted)).toEqual([
232
+ "Adopted (1) — already in the project, now in the stack instead of created again:",
233
+ ` service account ${GATEWAY_EMAIL} (caelo-production-gateway-sa)`,
234
+ ]);
235
+ expect(formatAdopted([])).toEqual([]);
236
+ });
237
+
238
+ it("a service account that does not exist stays a plain create, and nothing is imported", async () => {
239
+ const plan = viuOnePlan();
240
+ const pulumi = fakePulumi(plan);
241
+ const { adopted, steps } = await installerRun(plan, fakeGcloud([]).run, pulumi);
242
+ expect(adopted).toEqual([]);
243
+ expect(steps).toBe(plan);
244
+ expect(pulumi.imports).toEqual([]);
245
+ expect(pulumi.previews).toBe(0);
246
+ expect(steps.filter((s) => s.op === "create" && s.type === SA)).toHaveLength(2);
247
+ });
248
+
249
+ it("adopts only what exists: one of the two", async () => {
250
+ const plan = viuOnePlan();
251
+ const pulumi = fakePulumi(plan);
252
+ const { steps } = await installerRun(plan, fakeGcloud([MCP_EMAIL]).run, pulumi);
253
+ expect(pulumi.imports.flat().map((i) => i.name)).toEqual(["caelo-production-mcp-sa"]);
254
+ expect(steps.filter((s) => s.op === "create" && s.type === SA).map((s) => s.name)).toEqual([
255
+ "caelo-production-gateway-sa",
256
+ ]);
257
+ });
258
+
259
+ it("an import failure stops the run loudly with a next step: no re-plan, no up", async () => {
260
+ const plan = viuOnePlan();
261
+ const pulumi = fakePulumi(plan, { importFails: true });
262
+ const run = installerRun(plan, fakeGcloud([GATEWAY_EMAIL]).run, pulumi);
263
+ await expect(run).rejects.toBeInstanceOf(AdoptionError);
264
+ await expect(run).rejects.toThrow(
265
+ `could not adopt service account ${GATEWAY_EMAIL} (already in the project) into the stack: pulumi import says "error: Preview failed: resource 'projects/x' does not exist". Nothing was applied. Re-run the installer`,
266
+ );
267
+ expect(pulumi.previews).toBe(0);
268
+ expect(pulumi.ups).toBe(0);
269
+ });
270
+
271
+ it("a probe that fails for another reason than NOT_FOUND stops the run before any import", async () => {
272
+ const plan = viuOnePlan();
273
+ const pulumi = fakePulumi(plan);
274
+ const gcloud = fakeGcloud([], {
275
+ [GATEWAY_EMAIL]: "ERROR: (gcloud.iam.service-accounts.describe) PERMISSION_DENIED",
276
+ });
277
+ const run = installerRun(plan, gcloud.run, pulumi);
278
+ await expect(run).rejects.toThrow(
279
+ `could not tell whether the service account ${GATEWAY_EMAIL} exists (gcloud: ERROR: (gcloud.iam.service-accounts.describe) PERMISSION_DENIED). Nothing was imported or applied.`,
280
+ );
281
+ expect(pulumi.imports).toEqual([]);
282
+ expect(pulumi.ups).toBe(0);
283
+ });
284
+
285
+ it("a re-plan that still creates an adopted resource stops the run", async () => {
286
+ const plan = viuOnePlan();
287
+ const pulumi = fakePulumi(plan);
288
+ const run = adoptExistingResources(plan, {
289
+ projectId: PROJECT,
290
+ gcloud: fakeGcloud([GATEWAY_EMAIL]).run,
291
+ preview: async () => plan,
292
+ importResources: pulumi.importResources,
293
+ });
294
+ await expect(run).rejects.toThrow(
295
+ "but the next preview still creates caelo-production-gateway-sa. Nothing was applied.",
296
+ );
297
+ });
298
+ });
299
+
300
+ describe("adoptionCandidates — only declared creates of an adoptable kind", () => {
301
+ const step = (over: Partial<PlannedStep> & Pick<PlannedStep, "type" | "name">): PlannedStep => ({
302
+ op: "create",
303
+ urn: `urn:pulumi:production::caelo-gcp::${over.type}::${over.name}`,
304
+ diffs: [],
305
+ replaceReasons: [],
306
+ inputs: {},
307
+ ...over,
308
+ });
309
+
310
+ it("leaves unknown and non-adoptable kinds, IAM members, updates and unnamed creates to Pulumi", () => {
311
+ const steps = [
312
+ step({ type: "gcp:storage/bucket:Bucket", name: "media", inputs: { name: "x-media" } }),
313
+ step({ type: "gcp:secretmanager/secret:Secret", name: "kek", inputs: { secretId: "k" } }),
314
+ step({ type: "gcp:cloudrunv2/job:Job", name: "job", inputs: { name: "j" } }),
315
+ step({ type: "gcp:projects/iAMMember:IAMMember", name: "m", inputs: { role: "r" } }),
316
+ step({ type: "acme:widget:Widget", name: "w", inputs: { accountId: "a" } }),
317
+ step({ type: SA, name: "updated", op: "update", inputs: { accountId: "caelo-run" } }),
318
+ step({ type: SA, name: "no-inputs", inputs: undefined }),
319
+ step({ type: SA, name: "computed-id", inputs: { accountId: undefined } }),
320
+ step({
321
+ type: SA,
322
+ name: "unknown-id",
323
+ inputs: { accountId: "04da6b54-80e4-46f7-96ec-b56ff0331ba9" },
324
+ }),
325
+ step({
326
+ type: SA,
327
+ name: "elsewhere",
328
+ inputs: { accountId: "caelo-run", project: "other-project" },
329
+ }),
330
+ ];
331
+ expect(adoptionCandidates(steps, PROJECT)).toEqual([]);
332
+ });
333
+
334
+ it("names a service account by its accountId in the install's project", () => {
335
+ const [c] = adoptionCandidates(
336
+ [step({ type: SA, name: "run", inputs: { accountId: "caelo-run" } })],
337
+ PROJECT,
338
+ );
339
+ expect(c?.importId).toBe(
340
+ `projects/${PROJECT}/serviceAccounts/caelo-run@${PROJECT}.iam.gserviceaccount.com`,
341
+ );
342
+ expect(c?.label).toBe("service account");
343
+ });
344
+ });
345
+
346
+ describe("preview steps carry a create's inputs for adoption", () => {
347
+ it("keeps the inputs of creates only", () => {
348
+ const c = collectPreviewSteps();
349
+ const event = (op: "create" | "update", name: string): EngineEvent => ({
350
+ sequence: 1,
351
+ timestamp: 1_760_000_000,
352
+ resourcePreEvent: {
353
+ metadata: {
354
+ op,
355
+ urn: `urn:pulumi:production::caelo-gcp::${SA}::${name}`,
356
+ type: SA,
357
+ provider: PROVIDER_URN,
358
+ new: {
359
+ type: SA,
360
+ urn: `urn:pulumi:production::caelo-gcp::${SA}::${name}`,
361
+ id: "",
362
+ parent: "",
363
+ provider: PROVIDER_URN,
364
+ inputs: { accountId: name },
365
+ outputs: {},
366
+ },
367
+ },
368
+ },
369
+ });
370
+ c.onEvent(event("create", "a"));
371
+ c.onEvent(event("update", "b"));
372
+ const [created, updated] = c.steps();
373
+ expect(created?.inputs).toEqual({ accountId: "a" });
374
+ expect(updated?.inputs).toBeUndefined();
375
+ });
376
+ });
377
+
378
+ describe("stackGcpProviderUrn — what adopted resources are imported under", () => {
379
+ it("is the state's one GCP provider", () => {
380
+ expect(
381
+ stackGcpProviderUrn([
382
+ { type: "pulumi:pulumi:Stack", urn: "urn:stack" },
383
+ { type: "pulumi:providers:gcp", urn: PROVIDER_URN },
384
+ ]),
385
+ ).toBe(PROVIDER_URN);
386
+ });
387
+
388
+ it("refuses none or several", () => {
389
+ expect(() => stackGcpProviderUrn([])).toThrow(AdoptionError);
390
+ expect(() =>
391
+ stackGcpProviderUrn([
392
+ { type: "pulumi:providers:gcp", urn: PROVIDER_URN },
393
+ { type: "pulumi:providers:gcp", urn: `${PROVIDER_URN}-2` },
394
+ ]),
395
+ ).toThrow("holds 2 GCP providers");
396
+ });
397
+ });