@caelo-cms/provisioning 0.10.60 → 0.10.62
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/caddy/Caddyfile.production +7 -0
- package/caddy/Caddyfile.staging +7 -0
- package/dist/admin-internal-answer.d.ts +12 -0
- package/dist/admin-internal-answer.d.ts.map +1 -0
- package/dist/admin-internal-answer.js +32 -0
- package/dist/admin-internal-answer.js.map +1 -0
- package/dist/admin-internal-google.d.ts +4 -0
- package/dist/admin-internal-google.d.ts.map +1 -0
- package/dist/admin-internal-google.js +161 -0
- package/dist/admin-internal-google.js.map +1 -0
- package/dist/admin-internal-self-hosted.d.ts +30 -0
- package/dist/admin-internal-self-hosted.d.ts.map +1 -0
- package/dist/admin-internal-self-hosted.js +92 -0
- package/dist/admin-internal-self-hosted.js.map +1 -0
- package/dist/admin-internal.d.ts +33 -12
- package/dist/admin-internal.d.ts.map +1 -1
- package/dist/admin-internal.js +27 -159
- package/dist/admin-internal.js.map +1 -1
- package/dist/agent/command.d.ts +7 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -0
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/lock.d.ts +13 -1
- package/dist/agent/lock.d.ts.map +1 -1
- package/dist/agent/lock.js +62 -5
- package/dist/agent/lock.js.map +1 -1
- package/dist/agent/run.d.ts +5 -1
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +8 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +192 -90
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +11 -1
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +95 -38
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +7 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +10 -7
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +21 -0
- package/dist/converge-job-preflight.d.ts.map +1 -0
- package/dist/converge-job-preflight.js +111 -0
- package/dist/converge-job-preflight.js.map +1 -0
- package/dist/converge-job.d.ts +8 -2
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +14 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/gateway-credentials.d.ts +5 -3
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +5 -8
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +29 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +66 -0
- package/dist/gcloud.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +10 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +3 -1
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +10 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +12 -0
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +113 -28
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-iap.d.ts +2 -0
- package/dist/mcp-iap.d.ts.map +1 -1
- package/dist/mcp-iap.js +50 -17
- package/dist/mcp-iap.js.map +1 -1
- package/dist/operator-access.d.ts +1 -2
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +1 -2
- package/dist/operator-access.js.map +1 -1
- package/dist/owner-reset.d.ts +16 -19
- package/dist/owner-reset.d.ts.map +1 -1
- package/dist/owner-reset.js +92 -19
- package/dist/owner-reset.js.map +1 -1
- package/dist/preview-plan.d.ts +71 -0
- package/dist/preview-plan.d.ts.map +1 -0
- package/dist/preview-plan.js +168 -0
- package/dist/preview-plan.js.map +1 -0
- package/dist/public-role-isolation.d.ts +2 -0
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +9 -3
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +4 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/self-hosted-internal-secret.d.ts +27 -0
- package/dist/self-hosted-internal-secret.d.ts.map +1 -0
- package/dist/self-hosted-internal-secret.js +69 -0
- package/dist/self-hosted-internal-secret.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -2
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +13 -1
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +26 -2
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-converge.d.ts +5 -2
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +73 -8
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +4 -1
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js.map +1 -1
- package/dist/state-backend.d.ts +3 -2
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +3 -2
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +17 -0
- package/dist/state-command.d.ts.map +1 -1
- package/dist/state-command.js +34 -5
- package/dist/state-command.js.map +1 -1
- package/dist/state-migrate.d.ts +14 -3
- package/dist/state-migrate.d.ts.map +1 -1
- package/dist/state-migrate.js +31 -7
- package/dist/state-migrate.js.map +1 -1
- package/dist/state-pulumi.d.ts +0 -2
- package/dist/state-pulumi.d.ts.map +1 -1
- package/dist/state-pulumi.js +19 -20
- package/dist/state-pulumi.js.map +1 -1
- package/dist/static-security-headers.d.ts +86 -0
- package/dist/static-security-headers.d.ts.map +1 -0
- package/dist/static-security-headers.js +126 -0
- package/dist/static-security-headers.js.map +1 -0
- package/dist/static-svg-gcp.d.ts +28 -0
- package/dist/static-svg-gcp.d.ts.map +1 -0
- package/dist/static-svg-gcp.js +106 -0
- package/dist/static-svg-gcp.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +3 -0
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +12 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/backup.d.ts +2 -0
- package/dist/upgrade/backup.d.ts.map +1 -1
- package/dist/upgrade/backup.js +5 -4
- package/dist/upgrade/backup.js.map +1 -1
- package/dist/upgrade/converge.d.ts +7 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +19 -7
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +38 -3
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +31 -9
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -5
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +114 -25
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +17 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +23 -12
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/reporter.d.ts +2 -0
- package/dist/upgrade/reporter.d.ts.map +1 -1
- package/dist/upgrade/reporter.js +1 -0
- package/dist/upgrade/reporter.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +11 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +5 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -2
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +22 -13
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +16 -3
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +9 -4
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +10 -8
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +1 -0
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +70 -23
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +118 -39
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +67 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-internal-answer.ts +43 -0
- package/src/admin-internal-google.ts +187 -0
- package/src/admin-internal-self-hosted.test.ts +70 -0
- package/src/admin-internal-self-hosted.ts +128 -0
- package/src/admin-internal.ts +59 -174
- package/src/admin-memory.test.ts +1 -0
- package/src/agent/command.ts +3 -0
- package/src/agent/lock-config.test.ts +57 -2
- package/src/agent/lock.ts +68 -5
- package/src/agent/run.test.ts +18 -1
- package/src/agent/run.ts +17 -6
- package/src/caddy-redirects.test.ts +1 -0
- package/src/caddy.ts +12 -0
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +102 -44
- package/src/compose.ts +12 -0
- package/src/converge-job-gcp.ts +16 -11
- package/src/converge-job-preflight.test.ts +108 -0
- package/src/converge-job-preflight.ts +124 -0
- package/src/converge-job.test.ts +50 -0
- package/src/converge-job.ts +38 -2
- package/src/gateway-credentials.test.ts +4 -3
- package/src/gateway-credentials.ts +6 -10
- package/src/gateway-env-isolation.test.ts +1 -0
- package/src/gcloud.test.ts +93 -1
- package/src/gcloud.ts +82 -0
- package/src/index.test.ts +38 -1
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +10 -1
- package/src/install-job.ts +5 -2
- package/src/install-state.ts +13 -0
- package/src/lifecycle.ts +134 -28
- package/src/mcp-iap.test.ts +66 -5
- package/src/mcp-iap.ts +55 -17
- package/src/operator-access.ts +1 -2
- package/src/owner-reset-providers.test.ts +370 -0
- package/src/owner-reset.test.ts +2 -2
- package/src/owner-reset.ts +108 -19
- package/src/preview-plan.test.ts +233 -0
- package/src/preview-plan.ts +226 -0
- package/src/public-role-isolation.test.ts +13 -2
- package/src/public-role-isolation.ts +14 -4
- package/src/release-contract.ts +4 -0
- package/src/self-hosted-compose.test.ts +25 -0
- package/src/self-hosted-internal-secret.test.ts +61 -0
- package/src/self-hosted-internal-secret.ts +80 -0
- package/src/self-hosted-release.ts +10 -3
- package/src/self-hosted-upgrade.test.ts +35 -1
- package/src/self-hosted-upgrade.ts +40 -3
- package/src/stack-contract.test.ts +1 -0
- package/src/stack-converge.test.ts +89 -5
- package/src/stack-converge.ts +81 -8
- package/src/staging-admin.test.ts +1 -1
- package/src/staging-admin.ts +2 -1
- package/src/state-backend.ts +3 -2
- package/src/state-command.test.ts +31 -0
- package/src/state-command.ts +35 -8
- package/src/state-migrate.test.ts +58 -7
- package/src/state-migrate.ts +48 -10
- package/src/state-pulumi.ts +20 -21
- package/src/static-security-headers.test.ts +124 -0
- package/src/static-security-headers.ts +154 -0
- package/src/static-svg-gcp.test.ts +133 -0
- package/src/static-svg-gcp.ts +140 -0
- package/src/upgrade/app-roll.ts +18 -7
- package/src/upgrade/backup.ts +14 -5
- package/src/upgrade/converge.ts +25 -7
- package/src/upgrade/gcp-lookup.test.ts +80 -1
- package/src/upgrade/gcp-lookup.ts +61 -11
- package/src/upgrade/health.test.ts +132 -8
- package/src/upgrade/health.ts +169 -31
- package/src/upgrade/release-images.test.ts +17 -0
- package/src/upgrade/release-images.ts +26 -14
- package/src/upgrade/reporter.ts +3 -0
- package/src/upgrade/upgrade.test.ts +137 -8
- package/src/upgrade/upgrade.ts +14 -2
- package/src/upgrader-gcp.ts +28 -13
- package/src/upgrader-iam-preflight.test.ts +6 -3
- package/src/upgrader-iam-preflight.ts +26 -5
- package/src/upgrader-job.ts +14 -6
- package/src/upgrader-reporter.ts +1 -0
- package/src/wizards/gcp-pulumi.test.ts +1 -1
- package/src/wizards/gcp-pulumi.ts +147 -38
- package/src/wizards/gcp.ts +89 -9
- package/src/wizards/stack-work-dir.test.ts +175 -0
- package/stacks/aws/index.ts +40 -0
- package/stacks/azure/index.ts +22 -1
- package/stacks/gcp/README.md +4 -0
- package/stacks/gcp/index.ts +6 -2
- package/stacks/gcp/Pulumi.production.yaml +0 -16
package/src/upgrader-gcp.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { acquireBucketStateLock, GcsStateStore, realSyncGcloud } from "./state-s
|
|
|
28
28
|
import type { AppRollPlan } from "./upgrade/app-roll.js";
|
|
29
29
|
import { findServiceUrl } from "./upgrade/gcp-lookup.js";
|
|
30
30
|
import { createIapHealthProbe } from "./upgrade/health.js";
|
|
31
|
-
import { restIamTester } from "./upgrader-iam-preflight.js";
|
|
31
|
+
import { type IamTester, restIamTester } from "./upgrader-iam-preflight.js";
|
|
32
32
|
import type { UpdateQueue, UpgraderJobDeps } from "./upgrader-job.js";
|
|
33
33
|
import type { JobLog } from "./upgrader-reporter.js";
|
|
34
34
|
import { UPGRADER_TASK_TIMEOUT_MINUTES } from "./upgrader-setup.js";
|
|
@@ -59,9 +59,16 @@ export async function upgraderDatabaseHost(
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* The IAP-backed deep health probe for `plan`'s install (the upgrader job;
|
|
62
|
-
* the converge job, #819, too).
|
|
62
|
+
* the converge job, #819, too). `accessJustChanged`: a converge right
|
|
63
|
+
* before changed access the probe relies on (health.ts); the upgrader job
|
|
64
|
+
* changes none.
|
|
63
65
|
*/
|
|
64
|
-
export async function healthProbeFor(
|
|
66
|
+
export async function healthProbeFor(
|
|
67
|
+
run: GcloudRunner,
|
|
68
|
+
http: HttpFetch,
|
|
69
|
+
plan: AppRollPlan,
|
|
70
|
+
context: { readonly accessJustChanged: boolean },
|
|
71
|
+
) {
|
|
65
72
|
const { install } = plan;
|
|
66
73
|
const urls: Partial<Record<"admin" | "gateway", string>> = {};
|
|
67
74
|
for (const s of plan.services) {
|
|
@@ -81,11 +88,26 @@ export async function healthProbeFor(run: GcloudRunner, http: HttpFetch, plan: A
|
|
|
81
88
|
adminServiceUrl: urls.admin as string,
|
|
82
89
|
gatewayServiceUrl: urls.gateway as string,
|
|
83
90
|
},
|
|
84
|
-
{ run, http, sleep: realSleep },
|
|
91
|
+
{ run, http, sleep: realSleep, accessJustChanged: context.accessJustChanged },
|
|
85
92
|
),
|
|
86
93
|
};
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The IAM self-check's tester as this job's own account: gcloud's token from
|
|
98
|
+
* the metadata server, so it sees exactly the job's grants (#818, #884).
|
|
99
|
+
*/
|
|
100
|
+
export function gcloudIamTester(run: GcloudRunner, http: HttpFetch): IamTester {
|
|
101
|
+
return restIamTester({
|
|
102
|
+
http,
|
|
103
|
+
token: async () => {
|
|
104
|
+
const t = await run(["auth", "print-access-token"]);
|
|
105
|
+
if (!t.ok) throw new Error(`gcloud auth print-access-token: ${t.stderr.trim()}`);
|
|
106
|
+
return t.stdout.trim();
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
/** Every dependency of {@link runUpgraderJob} except the database and the log. */
|
|
90
112
|
export function gcpUpgraderDeps(
|
|
91
113
|
placement: UpgraderPlacement,
|
|
@@ -109,14 +131,7 @@ export function gcpUpgraderDeps(
|
|
|
109
131
|
execution: placement.execution,
|
|
110
132
|
// #818 — the IAM self-check asks as the job's own account (gcloud's
|
|
111
133
|
// token from the metadata server), so it sees exactly the job's grants.
|
|
112
|
-
iam:
|
|
113
|
-
http,
|
|
114
|
-
token: async () => {
|
|
115
|
-
const t = await run(["auth", "print-access-token"]);
|
|
116
|
-
if (!t.ok) throw new Error(`gcloud auth print-access-token: ${t.stderr.trim()}`);
|
|
117
|
-
return t.stdout.trim();
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
134
|
+
iam: gcloudIamTester(run, http),
|
|
120
135
|
// The default deps: the fixed registry and signer, never overridden.
|
|
121
136
|
fetchManifest: (version) => fetchVerifiedReleaseManifest(version),
|
|
122
137
|
// #815 — the lock names this execution, so a later run can break it
|
|
@@ -155,7 +170,7 @@ export function gcpUpgraderDeps(
|
|
|
155
170
|
{ projectId: placement.projectId, region: placement.region, imageOverride: imageRef },
|
|
156
171
|
{ run },
|
|
157
172
|
),
|
|
158
|
-
healthProbe: (plan) => healthProbeFor(run, http, plan),
|
|
173
|
+
healthProbe: (plan) => healthProbeFor(run, http, plan, { accessJustChanged: false }),
|
|
159
174
|
now: () => new Date(),
|
|
160
175
|
log,
|
|
161
176
|
};
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
restIamTester,
|
|
20
20
|
runIamPreflight,
|
|
21
21
|
testIamCall,
|
|
22
|
+
UPGRADER_PREFLIGHT_VOICE,
|
|
22
23
|
upgraderAccessChecks,
|
|
23
24
|
} from "./upgrader-iam-preflight.js";
|
|
24
25
|
import {
|
|
@@ -127,7 +128,9 @@ describe("upgraderAccessChecks", () => {
|
|
|
127
128
|
["iam.serviceAccounts.actAs"],
|
|
128
129
|
],
|
|
129
130
|
]);
|
|
130
|
-
expect(preflightMissingReason(r.missing)).toContain(
|
|
131
|
+
expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain(
|
|
132
|
+
"quality-audit job",
|
|
133
|
+
);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
it("never needs a permission only a forbidden role would give (no compute, no IAM admin)", () => {
|
|
@@ -185,7 +188,7 @@ describe("runIamPreflight", () => {
|
|
|
185
188
|
"run.jobs.run",
|
|
186
189
|
"storage.objects.list",
|
|
187
190
|
]);
|
|
188
|
-
const reason = preflightMissingReason(r.missing);
|
|
191
|
+
const reason = preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE);
|
|
189
192
|
expect(reason).toContain("run the database migrations");
|
|
190
193
|
expect(reason).toContain("missing storage.objects.list on gs://acme-caelo-ops");
|
|
191
194
|
expect(reason).toContain("Run the upgrade once with Cloud Shell");
|
|
@@ -209,7 +212,7 @@ describe("runIamPreflight", () => {
|
|
|
209
212
|
if (r.kind !== "missing") return;
|
|
210
213
|
expect(r.missing.flatMap((m) => m.missing)).not.toContain("storage.objects.get");
|
|
211
214
|
expect(r.missing.flatMap((m) => m.unchecked ?? [])).toEqual(["storage.objects.get"]);
|
|
212
|
-
expect(preflightMissingReason(r.missing)).toContain(
|
|
215
|
+
expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain(
|
|
213
216
|
"storage.objects.get on gs://acme-caelo-ops/install/ could not be checked",
|
|
214
217
|
);
|
|
215
218
|
});
|
|
@@ -84,7 +84,7 @@ export type IamResource =
|
|
|
84
84
|
|
|
85
85
|
/** One thing the run will do, the permissions it takes, and where. */
|
|
86
86
|
export interface AccessCheck {
|
|
87
|
-
/** What the
|
|
87
|
+
/** What the job could not do without them, in plain words. */
|
|
88
88
|
readonly need: string;
|
|
89
89
|
readonly resource: IamResource;
|
|
90
90
|
readonly permissions: readonly string[];
|
|
@@ -304,11 +304,32 @@ export async function runIamPreflight(
|
|
|
304
304
|
const OBJECT_LIST = "storage.objects.list";
|
|
305
305
|
const OBJECT_GET = "storage.objects.get";
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Who ran a self-check and how its access is repaired, for the request's
|
|
309
|
+
* end reason. The upgrader and the converge job (converge-job-preflight.ts)
|
|
310
|
+
* share the check; their identities and their repair differ.
|
|
311
|
+
*/
|
|
312
|
+
export interface PreflightVoice {
|
|
313
|
+
/** The job, as the sentence names it: "the upgrader". */
|
|
314
|
+
readonly subject: string;
|
|
315
|
+
/** The one next step that re-converges its grants. */
|
|
316
|
+
readonly repair: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The upgrader job's voice: Cloud Shell's upgrade re-converges upgrader-setup.ts. */
|
|
320
|
+
export const UPGRADER_PREFLIGHT_VOICE: PreflightVoice = {
|
|
321
|
+
subject: "the upgrader",
|
|
322
|
+
repair: "Run the upgrade once with Cloud Shell; it repairs the upgrader's access.",
|
|
323
|
+
};
|
|
324
|
+
|
|
307
325
|
/**
|
|
308
326
|
* The request's end reason for missing permissions: every one of them, each
|
|
309
327
|
* with what it is for, then the one next step.
|
|
310
328
|
*/
|
|
311
|
-
export function preflightMissingReason(
|
|
329
|
+
export function preflightMissingReason(
|
|
330
|
+
missing: readonly MissingAccess[],
|
|
331
|
+
voice: PreflightVoice,
|
|
332
|
+
): string {
|
|
312
333
|
const lines = missing.map((m) => {
|
|
313
334
|
const where = resourceName(m.check.resource);
|
|
314
335
|
const parts = [
|
|
@@ -321,12 +342,12 @@ export function preflightMissingReason(missing: readonly MissingAccess[]): strin
|
|
|
321
342
|
];
|
|
322
343
|
return `it ${m.missing.length > 0 ? "cannot" : "may not be able to"} ${m.check.need} (${parts.join("; ")})`;
|
|
323
344
|
});
|
|
324
|
-
return `Nothing was changed:
|
|
345
|
+
return `Nothing was changed: ${voice.subject} checked its access before starting and is missing permissions this update needs — ${lines.join("; ")}. ${voice.repair}`;
|
|
325
346
|
}
|
|
326
347
|
|
|
327
348
|
/** The request's end reason when IAM could not be asked. */
|
|
328
|
-
export function preflightErrorReason(errors: readonly string[]): string {
|
|
329
|
-
return `Nothing was changed:
|
|
349
|
+
export function preflightErrorReason(errors: readonly string[], voice: PreflightVoice): string {
|
|
350
|
+
return `Nothing was changed: ${voice.subject} could not check its access before starting (${errors.join("; ")}). Try again in a few minutes; if it keeps failing: ${voice.repair}`;
|
|
330
351
|
}
|
|
331
352
|
|
|
332
353
|
/** A resource's full name, as IAM and the messages name it. */
|
package/src/upgrader-job.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
71
71
|
import {
|
|
72
72
|
findServingImage,
|
|
73
|
+
lookupFailure,
|
|
73
74
|
resolveGcpResourceName,
|
|
74
75
|
servingImageDigests,
|
|
75
76
|
} from "./upgrade/gcp-lookup.js";
|
|
@@ -81,6 +82,7 @@ import {
|
|
|
81
82
|
preflightErrorReason,
|
|
82
83
|
preflightMissingReason,
|
|
83
84
|
runIamPreflight,
|
|
85
|
+
UPGRADER_PREFLIGHT_VOICE,
|
|
84
86
|
upgraderAccessChecks,
|
|
85
87
|
} from "./upgrader-iam-preflight.js";
|
|
86
88
|
import {
|
|
@@ -467,11 +469,15 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
467
469
|
return stopWith(
|
|
468
470
|
"needs_cloud_shell",
|
|
469
471
|
["iam-preflight"],
|
|
470
|
-
preflightMissingReason(access.missing),
|
|
472
|
+
preflightMissingReason(access.missing, UPGRADER_PREFLIGHT_VOICE),
|
|
471
473
|
);
|
|
472
474
|
}
|
|
473
475
|
if (access.kind === "error") {
|
|
474
|
-
return stopWith(
|
|
476
|
+
return stopWith(
|
|
477
|
+
"failed",
|
|
478
|
+
["iam-preflight"],
|
|
479
|
+
preflightErrorReason(access.errors, UPGRADER_PREFLIGHT_VOICE),
|
|
480
|
+
);
|
|
475
481
|
}
|
|
476
482
|
r.info("The upgrader has every permission this update needs.");
|
|
477
483
|
|
|
@@ -491,6 +497,7 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
491
497
|
env: deps.env,
|
|
492
498
|
domain: meta.domain,
|
|
493
499
|
stagingHost: meta.stagingDomain ?? `staging.${meta.domain}`,
|
|
500
|
+
credentials: "job",
|
|
494
501
|
},
|
|
495
502
|
images,
|
|
496
503
|
deps.run,
|
|
@@ -510,7 +517,7 @@ export async function runUpgraderJob(deps: UpgraderJobDeps): Promise<UpgraderRun
|
|
|
510
517
|
const sBak = r.step("Backing up the database before anything changes...");
|
|
511
518
|
const backup = await takeSqlBackup(
|
|
512
519
|
deps.run,
|
|
513
|
-
{ projectId: deps.projectId, env: deps.env },
|
|
520
|
+
{ projectId: deps.projectId, env: deps.env, credentials: "job" },
|
|
514
521
|
`pre-upgrade to ${m.version}`,
|
|
515
522
|
deps.now,
|
|
516
523
|
);
|
|
@@ -665,9 +672,10 @@ async function servingDigests(
|
|
|
665
672
|
deps.projectId,
|
|
666
673
|
deps.region,
|
|
667
674
|
);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
675
|
+
if (service.status !== "found") {
|
|
676
|
+
return { ok: false, error: lookupFailure(service, `${prefix}* Cloud Run service`, "job") };
|
|
677
|
+
}
|
|
678
|
+
const image = await findServingImage(deps.run, deps.projectId, deps.region, service.name);
|
|
671
679
|
if (!image) return { ok: false, error: `could not read which image the ${slug} serves` };
|
|
672
680
|
digests[slug] = servingImageDigests(image);
|
|
673
681
|
}
|
package/src/upgrader-reporter.ts
CHANGED
|
@@ -96,6 +96,7 @@ export function eventReporter(sink: EventSink, log: JobLog, requestId: string):
|
|
|
96
96
|
done: (m) => emit("success", m),
|
|
97
97
|
failed: (m) => emit("error", m),
|
|
98
98
|
warned: (m) => emit("warn", m),
|
|
99
|
+
progress: (m) => emit("info", m),
|
|
99
100
|
};
|
|
100
101
|
},
|
|
101
102
|
info: (m) => emit("info", m),
|
|
@@ -193,7 +193,7 @@ describe("pulumiGcpEnv with the install's ops bucket (#696)", () => {
|
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
describe("alignedStackSettings (#696 — the
|
|
196
|
+
describe("alignedStackSettings (#696 — a settings file the state moved away from)", () => {
|
|
197
197
|
const KMS =
|
|
198
198
|
"gcpkms://projects/acme/locations/europe-west1/keyRings/caelo/cryptoKeys/pulumi-state";
|
|
199
199
|
const gcs = { kind: "gcs" as const, secretsProvider: KMS };
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* config and up; failures surface inline with provider errors.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, relative, resolve as resolvePath } from "node:path";
|
|
15
16
|
import * as pulumi from "@pulumi/pulumi/automation";
|
|
16
17
|
import { AUDIT_PARALLELISM_DEFAULT } from "../audit-job-setup.js";
|
|
17
18
|
import { checkpointableOutputs } from "../checkpoint-secrets.js";
|
|
18
19
|
import { GCP_STACK_ENV } from "../gcp-names.js";
|
|
19
|
-
import type
|
|
20
|
+
import { type ImageDigests, stackWorkDir } from "../install-state.js";
|
|
21
|
+
import { collectPreviewSteps, type PlannedStep } from "../preview-plan.js";
|
|
20
22
|
import {
|
|
21
23
|
AMBIENT_GOOGLE_CREDENTIAL_ENV,
|
|
22
24
|
withoutAmbientGoogleCredentials,
|
|
@@ -102,18 +104,99 @@ export interface PulumiUpResult {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
107
|
+
* The stack program shipped in the package (`stacks/<provider>/`: its
|
|
108
|
+
* Pulumi.yaml and index.ts), whether the CLI runs from source or from the
|
|
109
|
+
* published tarball. Read-only at runtime: Pulumi runs in the install's own
|
|
110
|
+
* work dir (install-state.ts `stackWorkDir`, {@link prepareStackWorkDir}),
|
|
111
|
+
* never here.
|
|
108
112
|
*/
|
|
109
|
-
export function
|
|
110
|
-
//
|
|
111
|
-
// tarball under `stacks/<provider>/`. From this file's location at
|
|
112
|
-
// runtime (dist/wizards/gcp-pulumi.js), the stack dir is two
|
|
113
|
-
// levels up + into the provider-specific stack folder.
|
|
113
|
+
export function packageStackDir(provider: "gcp" | "gcp-firebase"): string {
|
|
114
|
+
// From this file at runtime (src/ or dist/wizards/), two levels up.
|
|
114
115
|
return resolvePath(import.meta.dir, `../../stacks/${provider}`);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The package's project file with `main` pointing from `workDir` at the
|
|
120
|
+
* package's stack program, so Pulumi runs that program (its relative
|
|
121
|
+
* imports, tsconfig and node_modules resolve from the package) while the
|
|
122
|
+
* stack settings file stays in `workDir`. Pulumi joins `main` to the
|
|
123
|
+
* project dir, so it must be relative — and computed between real paths:
|
|
124
|
+
* Pulumi resolves the project dir through symlinks (macOS `/var` →
|
|
125
|
+
* `/private/var`, a symlinked home or package cache), so a path relative to
|
|
126
|
+
* the symlinked spelling points nowhere.
|
|
127
|
+
*
|
|
128
|
+
* @param programDir the package's stack dir, symlinks resolved
|
|
129
|
+
* @param workDir the install's work dir, symlinks resolved
|
|
130
|
+
*/
|
|
131
|
+
export function workDirProjectFile(
|
|
132
|
+
projectYaml: string,
|
|
133
|
+
programDir: string,
|
|
134
|
+
workDir: string,
|
|
135
|
+
): string {
|
|
136
|
+
const mains = [...projectYaml.matchAll(/^main:\s*(\S+)\s*$/gm)];
|
|
137
|
+
if (mains.length !== 1) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${join(programDir, "Pulumi.yaml")} must name its program in exactly one top-level "main:" line (found ${mains.length}); the CLI points the install's stack work dir at it.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const program = resolvePath(programDir, (mains[0] as RegExpMatchArray)[1] as string);
|
|
143
|
+
return projectYaml.replace(/^main:\s*\S+\s*$/m, `main: ${relative(workDir, program)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create or refresh the install's stack work dir (install-state.ts
|
|
148
|
+
* `stackWorkDir`) and
|
|
149
|
+
* return it. The project file is rewritten on every run: each `bunx`
|
|
150
|
+
* version unpacks the package somewhere else. The settings file is left as
|
|
151
|
+
* it is (it is the install's own). Atomic (temp file + rename), so two runs
|
|
152
|
+
* for the same install never leave a half-written project file.
|
|
153
|
+
*/
|
|
154
|
+
export function prepareStackWorkDir(
|
|
155
|
+
installRoot: string,
|
|
156
|
+
provider: "gcp" | "gcp-firebase",
|
|
157
|
+
programDir: string = packageStackDir(provider),
|
|
158
|
+
): string {
|
|
159
|
+
const workDir = stackWorkDir(installRoot, provider);
|
|
160
|
+
mkdirSync(workDir, { recursive: true, mode: 0o700 });
|
|
161
|
+
const project = workDirProjectFile(
|
|
162
|
+
readFileSync(join(programDir, "Pulumi.yaml"), "utf8"),
|
|
163
|
+
realpathSync(programDir),
|
|
164
|
+
realpathSync(workDir),
|
|
165
|
+
);
|
|
166
|
+
const target = join(workDir, "Pulumi.yaml");
|
|
167
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
168
|
+
writeFileSync(tmp, project, { mode: 0o600 });
|
|
169
|
+
renameSync(tmp, target);
|
|
170
|
+
return workDir;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Refuse to run Pulumi when the stack config names another project than
|
|
175
|
+
* the install's (#736, CLAUDE.md §2): a preview or `up` would plan the
|
|
176
|
+
* install's state against another install's project — replacing its
|
|
177
|
+
* Firebase site, buckets and IAM members. Defense in depth: each install
|
|
178
|
+
* has its own work dir, so nothing should ever put another project there.
|
|
179
|
+
*/
|
|
180
|
+
export async function assertStackProject(
|
|
181
|
+
stack: pulumi.Stack,
|
|
182
|
+
provider: "gcp" | "gcp-firebase",
|
|
183
|
+
projectId: string,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const key = `caelo-${provider}:project`;
|
|
186
|
+
const configured = await stack.getConfig(key).then(
|
|
187
|
+
(v) => v.value,
|
|
188
|
+
(e: unknown) => {
|
|
189
|
+
if (/not found/i.test(String(e))) return undefined;
|
|
190
|
+
throw e;
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
if (configured !== projectId) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`the stack config ${join(stack.workspace.workDir, `Pulumi.${GCP_STACK_ENV}.yaml`)} names project ${configured ?? "(none)"}, but this install is ${projectId}. Nothing was run. Delete that file and re-run: the CLI writes the install's config again.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
117
200
|
/**
|
|
118
201
|
* The stack config keys that pin each Cloud Run service's image
|
|
119
202
|
* (`<ns>:image-digest-<service>`, read by `imageRef` / `imageTag` in the
|
|
@@ -144,16 +227,17 @@ export function imageDigestConfig(
|
|
|
144
227
|
* @returns the override keys it removed.
|
|
145
228
|
*/
|
|
146
229
|
export async function writeImageDigestsToStack(inputs: {
|
|
230
|
+
installRoot: string;
|
|
147
231
|
backend: ResolvedBackend;
|
|
148
232
|
provider: "gcp" | "gcp-firebase";
|
|
149
233
|
digests: ImageDigests;
|
|
150
234
|
}): Promise<{ removedOverrides: string[] }> {
|
|
151
|
-
const stack = await selectInstallStack(inputs.backend, inputs.provider);
|
|
235
|
+
const stack = await selectInstallStack(inputs.installRoot, inputs.backend, inputs.provider);
|
|
152
236
|
await stack.setAllConfig(imageDigestConfig(inputs.provider, inputs.digests));
|
|
153
237
|
// Read the settings file, not `getAllConfig()`: `pulumi config` refuses
|
|
154
238
|
// to list a stack whose required keys (domain, project, …) are unset, and
|
|
155
|
-
//
|
|
156
|
-
//
|
|
239
|
+
// a settings file the installer never wrote (an install from before the
|
|
240
|
+
// per-install work dir, #736) holds only what this call just wrote.
|
|
157
241
|
const settings = await stack.workspace.stackSettings(stack.name);
|
|
158
242
|
const removedOverrides = presentImageOverrides(inputs.provider, settings.config);
|
|
159
243
|
if (removedOverrides.length > 0) await stack.removeAllConfig(removedOverrides);
|
|
@@ -180,24 +264,24 @@ export function presentImageOverrides(
|
|
|
180
264
|
* @returns the output's value, or undefined when the stack has no such output
|
|
181
265
|
*/
|
|
182
266
|
export async function readStackOutput(inputs: {
|
|
267
|
+
installRoot: string;
|
|
183
268
|
backend: ResolvedBackend;
|
|
184
269
|
provider: "gcp" | "gcp-firebase";
|
|
185
270
|
name: string;
|
|
186
271
|
}): Promise<unknown> {
|
|
187
|
-
const stack = await selectInstallStack(inputs.backend, inputs.provider);
|
|
272
|
+
const stack = await selectInstallStack(inputs.installRoot, inputs.backend, inputs.provider);
|
|
188
273
|
const outputs = await stack.outputs();
|
|
189
274
|
return outputs[inputs.name]?.value;
|
|
190
275
|
}
|
|
191
276
|
|
|
192
277
|
/**
|
|
193
|
-
* The
|
|
194
|
-
*
|
|
195
|
-
* already fine (#696).
|
|
278
|
+
* The install's stack settings file with secrets settings that can't
|
|
279
|
+
* contradict its state, or null when it is already fine (#696).
|
|
196
280
|
*
|
|
197
|
-
* That file (`Pulumi.production.yaml
|
|
198
|
-
* state:
|
|
199
|
-
* provider
|
|
200
|
-
*
|
|
281
|
+
* That file (`Pulumi.production.yaml`, in the install's work dir) is not
|
|
282
|
+
* the state: `state migrate` moves the state to another backend and secrets
|
|
283
|
+
* provider, and the file can still name the old passphrase salt or KMS key.
|
|
284
|
+
* A file naming anything but the install's own
|
|
201
285
|
* KMS key loses its secrets-provider fields and the `secure:` values
|
|
202
286
|
* encrypted under them (no stack config the CLI sets is secret);
|
|
203
287
|
* `PULUMI_FALLBACK_TO_STATE_SECRETS_MANAGER` (resolveBackend) then makes
|
|
@@ -230,17 +314,16 @@ export function alignedStackSettings(
|
|
|
230
314
|
}
|
|
231
315
|
|
|
232
316
|
/**
|
|
233
|
-
* Bring the
|
|
317
|
+
* Bring the install's stack settings file in line with its backend
|
|
234
318
|
* ({@link alignedStackSettings}) before Pulumi reads it.
|
|
319
|
+
*
|
|
320
|
+
* @param workDir the install's prepared work dir ({@link prepareStackWorkDir})
|
|
235
321
|
*/
|
|
236
322
|
export async function alignStackSettingsFile(
|
|
323
|
+
workDir: string,
|
|
237
324
|
backend: ResolvedBackend,
|
|
238
|
-
provider: "gcp" | "gcp-firebase",
|
|
239
325
|
): Promise<void> {
|
|
240
|
-
const ws = await pulumi.LocalWorkspace.create({
|
|
241
|
-
workDir: gcpStackWorkDir(provider),
|
|
242
|
-
envVars: stateOnlyEnv(backend),
|
|
243
|
-
});
|
|
326
|
+
const ws = await pulumi.LocalWorkspace.create({ workDir, envVars: stateOnlyEnv(backend) });
|
|
244
327
|
let settings: pulumi.StackSettings;
|
|
245
328
|
try {
|
|
246
329
|
settings = await ws.stackSettings(GCP_STACK_ENV);
|
|
@@ -266,14 +349,16 @@ export function stateOnlyEnv(backend: Pick<ResolvedBackend, "envVars">): Record<
|
|
|
266
349
|
};
|
|
267
350
|
}
|
|
268
351
|
|
|
269
|
-
/** The install's existing stack, on its backend. */
|
|
352
|
+
/** The install's existing stack, on its backend, in its own work dir. */
|
|
270
353
|
export async function selectInstallStack(
|
|
354
|
+
installRoot: string,
|
|
271
355
|
backend: ResolvedBackend,
|
|
272
356
|
provider: "gcp" | "gcp-firebase",
|
|
273
357
|
): Promise<pulumi.Stack> {
|
|
274
|
-
|
|
358
|
+
const workDir = prepareStackWorkDir(installRoot, provider);
|
|
359
|
+
await alignStackSettingsFile(workDir, backend);
|
|
275
360
|
return pulumi.LocalWorkspace.selectStack(
|
|
276
|
-
{ stackName: GCP_STACK_ENV, workDir
|
|
361
|
+
{ stackName: GCP_STACK_ENV, workDir },
|
|
277
362
|
{ envVars: stateOnlyEnv(backend) },
|
|
278
363
|
);
|
|
279
364
|
}
|
|
@@ -318,8 +403,8 @@ export interface StateResource {
|
|
|
318
403
|
/**
|
|
319
404
|
* The region an existing stack was deployed in, read from its state (the
|
|
320
405
|
* stacks' explicit `gcp.Provider` carries it), or null for a stack with
|
|
321
|
-
* nothing deployed. State, not stack config: the
|
|
322
|
-
*
|
|
406
|
+
* nothing deployed. State, not stack config: the state is authoritative,
|
|
407
|
+
* and the settings file of an install from before #736 starts empty.
|
|
323
408
|
*/
|
|
324
409
|
export function deployedStackRegion(resources: ReadonlyArray<StateResource>): string | null {
|
|
325
410
|
const regions = new Set(
|
|
@@ -368,10 +453,10 @@ export type StackConfigInputs = Pick<
|
|
|
368
453
|
|
|
369
454
|
/**
|
|
370
455
|
* Set every config value the stack reads, then its `secretReplication`
|
|
371
|
-
* ({@link resolveSecretReplication}). The
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
456
|
+
* ({@link resolveSecretReplication}). The wizard sets all of it before every
|
|
457
|
+
* `up`, so install.json stays the source of truth; `state migrate` sets it
|
|
458
|
+
* before the previews that prove a migration changed nothing (#696) when
|
|
459
|
+
* the install's settings file does not hold it yet.
|
|
375
460
|
*
|
|
376
461
|
* @returns the secret replication now configured
|
|
377
462
|
*/
|
|
@@ -449,22 +534,34 @@ export function pulumiGcpEnv(
|
|
|
449
534
|
};
|
|
450
535
|
}
|
|
451
536
|
|
|
537
|
+
/** Thrown when the review (preview-plan.ts planGate) stops `pulumi up` after the preview. */
|
|
538
|
+
export class PulumiUpDeclinedError extends Error {
|
|
539
|
+
override readonly name = "PulumiUpDeclinedError";
|
|
540
|
+
}
|
|
541
|
+
|
|
452
542
|
/**
|
|
453
543
|
* Run `pulumi up` against the GCP stack via the Automation SDK.
|
|
454
544
|
* Streams resource-create events to the supplied onEvent callback so
|
|
455
545
|
* the wizard can render a live progress bar.
|
|
546
|
+
*
|
|
547
|
+
* On a stack that already holds resources, a preview runs first (#737) and
|
|
548
|
+
* `review` sees its planned steps: it lists them and decides (planGate)
|
|
549
|
+
* whether `up` may apply them. Nothing is applied when it says no.
|
|
550
|
+
*
|
|
551
|
+
* @throws PulumiUpDeclinedError when `review` declines the plan
|
|
456
552
|
*/
|
|
457
553
|
export async function pulumiUpGcp(
|
|
458
554
|
inputs: PulumiUpInputs,
|
|
459
555
|
onEvent: (kind: "resource" | "error" | "log", message: string) => void,
|
|
556
|
+
review: (steps: readonly PlannedStep[]) => Promise<boolean>,
|
|
460
557
|
): Promise<PulumiUpResult> {
|
|
461
558
|
const env = pulumiGcpEnv(process.env, inputs);
|
|
462
559
|
|
|
463
560
|
const provider = inputs.provider ?? "gcp";
|
|
464
561
|
const stackName = GCP_STACK_ENV;
|
|
465
|
-
const workDir =
|
|
562
|
+
const workDir = prepareStackWorkDir(inputs.installRoot, provider);
|
|
466
563
|
|
|
467
|
-
await alignStackSettingsFile(inputs.backend
|
|
564
|
+
await alignStackSettingsFile(workDir, inputs.backend);
|
|
468
565
|
const workspaceOpts = {
|
|
469
566
|
envVars: env,
|
|
470
567
|
// Used when the stack is created: a new install's secrets are
|
|
@@ -506,8 +603,20 @@ export async function pulumiUpGcp(
|
|
|
506
603
|
}
|
|
507
604
|
|
|
508
605
|
// Refresh state first to detect drift from any out-of-band changes.
|
|
606
|
+
await assertStackProject(stack, provider, inputs.projectId);
|
|
509
607
|
await stack.refresh({ onOutput: (msg) => onEvent("log", msg) });
|
|
510
608
|
|
|
609
|
+
// #737 — never `up` a deployed stack blind: list what it would change
|
|
610
|
+
// (replaces and deletes first) and let the review stop it. A first run
|
|
611
|
+
// on an empty stack only creates, so it skips the extra preview.
|
|
612
|
+
if (stateResources.length > 0) {
|
|
613
|
+
const collector = collectPreviewSteps();
|
|
614
|
+
await stack.preview({ onEvent: collector.onEvent });
|
|
615
|
+
if (!(await review(collector.steps()))) {
|
|
616
|
+
throw new PulumiUpDeclinedError("pulumi up was not run: the planned changes were declined.");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
511
620
|
// Run `up`. The onEvent callback receives the same JSON-RPC events
|
|
512
621
|
// Pulumi prints to stdout in --json mode.
|
|
513
622
|
const result = await stack.up({
|