@caelo-cms/provisioning 0.10.55 → 0.10.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-db.js +97 -24
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +66 -0
- package/dist/converge-checkpoint.d.ts.map +1 -0
- package/dist/converge-checkpoint.js +61 -0
- package/dist/converge-checkpoint.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +46 -0
- package/dist/converge-job-gcp.d.ts.map +1 -0
- package/dist/converge-job-gcp.js +368 -0
- package/dist/converge-job-gcp.js.map +1 -0
- package/dist/converge-job-setup.d.ts +62 -0
- package/dist/converge-job-setup.d.ts.map +1 -0
- package/dist/converge-job-setup.js +228 -0
- package/dist/converge-job-setup.js.map +1 -0
- package/dist/converge-job.d.ts +141 -0
- package/dist/converge-job.d.ts.map +1 -0
- package/dist/converge-job.js +325 -0
- package/dist/converge-job.js.map +1 -0
- package/dist/gcp-names.d.ts +9 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +13 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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 +33 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +11 -3
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +22 -12
- package/dist/install-job.js.map +1 -1
- package/dist/lifecycle.d.ts +13 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +115 -58
- package/dist/lifecycle.js.map +1 -1
- package/dist/operator-access.d.ts +4 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +43 -7
- package/dist/operator-access.js.map +1 -1
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +5 -2
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +2 -0
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp-edge.d.ts +114 -0
- package/dist/remote-mcp-edge.d.ts.map +1 -0
- package/dist/remote-mcp-edge.js +116 -0
- package/dist/remote-mcp-edge.js.map +1 -0
- package/dist/remote-mcp.d.ts +139 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +434 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/runtime-secrets.d.ts +12 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +32 -4
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +38 -0
- package/dist/seal-key-rotation.d.ts.map +1 -0
- package/dist/seal-key-rotation.js +206 -0
- package/dist/seal-key-rotation.js.map +1 -0
- package/dist/stack-contract.d.ts +206 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +219 -4
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +53 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +124 -4
- package/dist/stack-converge.js.map +1 -1
- package/dist/state-lock-lease.d.ts +142 -0
- package/dist/state-lock-lease.d.ts.map +1 -0
- package/dist/state-lock-lease.js +347 -0
- package/dist/state-lock-lease.js.map +1 -0
- package/dist/state-store.d.ts +66 -9
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +249 -30
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +36 -7
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +53 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/audit-job-roll.d.ts.map +1 -1
- package/dist/upgrade/audit-job-roll.js +3 -1
- package/dist/upgrade/audit-job-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +125 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +27 -10
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +46 -7
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/record-rolled.d.ts +40 -0
- package/dist/upgrade/record-rolled.d.ts.map +1 -0
- package/dist/upgrade/record-rolled.js +46 -0
- package/dist/upgrade/record-rolled.js.map +1 -0
- package/dist/upgrade/upgrade.d.ts +45 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +60 -17
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +15 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +36 -6
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +15 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +42 -7
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +123 -0
- package/dist/upgrader-iam-preflight.d.ts.map +1 -0
- package/dist/upgrader-iam-preflight.js +414 -0
- package/dist/upgrader-iam-preflight.js.map +1 -0
- package/dist/upgrader-job.d.ts +33 -9
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +169 -27
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +58 -0
- package/dist/upgrader-records.d.ts.map +1 -0
- package/dist/upgrader-records.js +87 -0
- package/dist/upgrader-records.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +3 -2
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +10 -2
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/upgrader-setup.d.ts +25 -3
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +123 -11
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +11 -3
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +6 -0
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +14 -2
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +106 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/run.test.ts +14 -1
- package/src/audit-job-setup.test.ts +7 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +4 -1
- package/src/compose.ts +5 -0
- package/src/converge-checkpoint.ts +75 -0
- package/src/converge-entrypoint.test.ts +213 -0
- package/src/converge-job-gcp.test.ts +222 -0
- package/src/converge-job-gcp.ts +443 -0
- package/src/converge-job-setup.test.ts +152 -0
- package/src/converge-job-setup.ts +268 -0
- package/src/converge-job.test.ts +486 -0
- package/src/converge-job.ts +542 -0
- package/src/gateway-env-isolation.test.ts +8 -1
- package/src/gcp-names.ts +15 -0
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +8 -4
- package/src/infra-contract.ts +33 -1
- package/src/install-job.test.ts +3 -1
- package/src/install-job.ts +31 -14
- package/src/lifecycle.ts +137 -71
- package/src/operator-access.test.ts +24 -1
- package/src/operator-access.ts +44 -6
- package/src/release-contract.ts +5 -2
- package/src/release-manifest.test.ts +42 -0
- package/src/release-manifest.ts +2 -0
- package/src/remote-mcp-edge.ts +137 -0
- package/src/remote-mcp.test.ts +690 -0
- package/src/remote-mcp.ts +524 -0
- package/src/runtime-secrets.test.ts +31 -6
- package/src/runtime-secrets.ts +41 -3
- package/src/seal-key-rotation.test.ts +131 -0
- package/src/seal-key-rotation.ts +258 -0
- package/src/stack-contract-parity.test.ts +20 -3
- package/src/stack-contract.test.ts +32 -2
- package/src/stack-contract.ts +363 -4
- package/src/stack-converge-secrets.test.ts +4 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +162 -7
- package/src/state-attach.test.ts +7 -1
- package/src/state-lock-lease.test.ts +302 -0
- package/src/state-lock-lease.ts +423 -0
- package/src/state-lock.test.ts +568 -0
- package/src/state-store.ts +347 -27
- package/src/upgrade/app-roll.ts +102 -21
- package/src/upgrade/audit-job-roll.ts +3 -1
- package/src/upgrade/converge.ts +148 -10
- package/src/upgrade/gcp-lookup.test.ts +80 -0
- package/src/upgrade/gcp-lookup.ts +65 -8
- package/src/upgrade/record-rolled.test.ts +105 -0
- package/src/upgrade/record-rolled.ts +91 -0
- package/src/upgrade/upgrade.test.ts +203 -3
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +92 -3
- package/src/upgrader-assess.ts +58 -9
- package/src/upgrader-gcp.ts +47 -8
- package/src/upgrader-iam-preflight.test.ts +393 -0
- package/src/upgrader-iam-preflight.ts +510 -0
- package/src/upgrader-job.test.ts +586 -15
- package/src/upgrader-job.ts +237 -53
- package/src/upgrader-records.ts +151 -0
- package/src/upgrader-reporter.test.ts +6 -0
- package/src/upgrader-reporter.ts +7 -2
- package/src/upgrader-setup.test.ts +3 -3
- package/src/upgrader-setup.ts +128 -11
- package/src/wizards/gcp-cost.ts +12 -3
- package/src/wizards/gcp-pulumi.test.ts +15 -0
- package/src/wizards/gcp-pulumi.ts +18 -2
- package/src/wizards/gcp.ts +134 -4
- package/stacks/aws/index.ts +10 -0
- package/stacks/azure/index.ts +9 -0
- package/stacks/gcp/index.ts +47 -7
- package/stacks/gcp-firebase/index.ts +38 -6
|
@@ -37,7 +37,9 @@ export async function describeAuditJobImage(
|
|
|
37
37
|
"--format=value(spec.template.spec.template.spec.containers[0].image)",
|
|
38
38
|
]);
|
|
39
39
|
if (!r.ok) {
|
|
40
|
-
|
|
40
|
+
// gcloud's own wording for a missing job is "Cannot find job [<name>]."
|
|
41
|
+
// (no NOT_FOUND code), so match that too.
|
|
42
|
+
return /NOT_FOUND|not found|could not be found|cannot find/i.test(r.stderr)
|
|
41
43
|
? { ok: true, image: null }
|
|
42
44
|
: { ok: false, error: r.stderr.trim() };
|
|
43
45
|
}
|
package/src/upgrade/converge.ts
CHANGED
|
@@ -7,17 +7,20 @@
|
|
|
7
7
|
* CLI-generated runtime secrets, the stack's IAM/CDN/firewall invariants,
|
|
8
8
|
* MCP access through IAP, public_role's Cloud SQL role memberships (#633),
|
|
9
9
|
* the gateway's and public_role's database logins, the operator-access sync
|
|
10
|
-
* job, the quality-audit job (#875),
|
|
11
|
-
* more.
|
|
10
|
+
* job, the quality-audit job (#875), the remote-MCP load-balancer rules and
|
|
11
|
+
* identity env (#806), and dropping bindings nothing reads any more.
|
|
12
12
|
*
|
|
13
|
-
* It needs the rights of whoever provisioned the install
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* It needs the rights of whoever provisioned the install: `cms-provision
|
|
14
|
+
* upgrade` runs it as the operator's own identity, and the converge job
|
|
15
|
+
* (#819, converge-job.ts) runs the same upgrade as the install's
|
|
16
|
+
* `caelo-provisioner` account. Never the in-cloud upgrader job, which only
|
|
17
|
+
* app-rolls and refuses while {@link needsConverge} says the install is
|
|
18
|
+
* behind.
|
|
17
19
|
*
|
|
18
20
|
* The keyless-provisioner step (#696, provisioner-identity.ts) is converge
|
|
19
21
|
* too; it needs the operator's local install dir, so the CLI injects it as
|
|
20
|
-
* {@link ConvergeDeps.provisionerIdentity}
|
|
22
|
+
* {@link ConvergeDeps.provisionerIdentity} (the converge job, which IS the
|
|
23
|
+
* provisioner, has nothing to move and passes none).
|
|
21
24
|
*
|
|
22
25
|
* Some steps must sit between app-roll steps (a role migration 0248 creates
|
|
23
26
|
* gets its password after migrations, before the gateway rolls onto it), so
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
32
|
+
import { ensureConvergeJob } from "../converge-job-setup.js";
|
|
29
33
|
import { findSqlInstance, planPublicRoleSwitch } from "../gateway-credentials.js";
|
|
30
34
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
31
35
|
import { upgraderServiceAccountEmail } from "../gcp-names.js";
|
|
@@ -33,6 +37,7 @@ import type { InstallMetadata } from "../install-state.js";
|
|
|
33
37
|
import { ensureMcpIapAccess, type IapResource } from "../mcp-iap.js";
|
|
34
38
|
import { ensureOperatorAccessSync, resolveOperatorAccessTarget } from "../operator-access.js";
|
|
35
39
|
import { revokePublicRoleMemberships } from "../public-role-isolation.js";
|
|
40
|
+
import { ensureRemoteMcpEdge, resolveRemoteMcpIdentity } from "../remote-mcp.js";
|
|
36
41
|
import {
|
|
37
42
|
ensureGatewayServiceAccount,
|
|
38
43
|
ensureGeneratedSecrets,
|
|
@@ -44,11 +49,15 @@ import { selfCommand } from "../self-command.js";
|
|
|
44
49
|
import {
|
|
45
50
|
AUDIT_JOB_ENV_VAR,
|
|
46
51
|
AUDIT_RESULTS_BUCKET_ENV_VAR,
|
|
52
|
+
type CloudRunEnvVar,
|
|
53
|
+
CONVERGE_JOB_ENV_VAR,
|
|
47
54
|
MCP_ENV_VAR,
|
|
48
55
|
OPERATOR_ACCESS_JOB_ENV_VAR,
|
|
56
|
+
remoteMcpIdentityEnv,
|
|
49
57
|
UPGRADER_JOB_ENV_VAR,
|
|
50
58
|
} from "../stack-contract.js";
|
|
51
59
|
import {
|
|
60
|
+
deleteRetiredSecrets,
|
|
52
61
|
ensureStackInvariants,
|
|
53
62
|
liveEnvHasInlinePassword,
|
|
54
63
|
removeRetiredIamBindings,
|
|
@@ -308,6 +317,75 @@ export function startConverge(
|
|
|
308
317
|
leaveUntouched.push(AUDIT_JOB_ENV_VAR, AUDIT_RESULTS_BUCKET_ENV_VAR);
|
|
309
318
|
}
|
|
310
319
|
|
|
320
|
+
async function remoteMcpEdge(): Promise<void> {
|
|
321
|
+
// #806 — on `gcp` the load balancer routes the remote-MCP discovery
|
|
322
|
+
// paths to the gateway and Cloud Armor lets agent traffic through on
|
|
323
|
+
// /api/mcp/* (remote-mcp-edge.ts). Only remote MCP depends on it, so a
|
|
324
|
+
// failure warns.
|
|
325
|
+
if (install.provider !== "gcp") return;
|
|
326
|
+
const s = r.step("Ensuring the load balancer's remote-MCP routes and Cloud Armor rule...");
|
|
327
|
+
const edge = await ensureRemoteMcpEdge(
|
|
328
|
+
{
|
|
329
|
+
provider: install.provider,
|
|
330
|
+
projectId: install.projectId,
|
|
331
|
+
env: install.env,
|
|
332
|
+
domain: install.domain,
|
|
333
|
+
},
|
|
334
|
+
{ ...runDeps, ...(deps.http ? { http: deps.http } : {}) },
|
|
335
|
+
);
|
|
336
|
+
if (edge.ok) {
|
|
337
|
+
for (const line of edge.done) r.info(` ${line}`);
|
|
338
|
+
s.done("Remote-MCP load-balancer rules ready");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
s.warned(`Remote-MCP load-balancer rules not set up: ${edge.error}`);
|
|
342
|
+
r.warn(
|
|
343
|
+
"The upgrade continues; remote MCP (Settings → MCP → Remote) cannot be reached through the site's domain until this succeeds. Re-run upgrade once the cause is fixed.",
|
|
344
|
+
);
|
|
345
|
+
incomplete = true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function remoteMcpIdentity(): Promise<Record<RollSlug, CloudRunEnvVar[]> | null> {
|
|
349
|
+
// #806 — CAELO_ADMIN_URL / CAELO_MCP_PUBLIC_URL / CAELO_IAP_AUDIENCE
|
|
350
|
+
// name identities the env contract cannot (stack-contract.ts
|
|
351
|
+
// RemoteMcpIdentity). Resolved here, read-only, and set by the rolls
|
|
352
|
+
// (AppRollHooks.afterMigrations `extraEnv`), so they ride the new
|
|
353
|
+
// revisions. A failure leaves the vars as the services have them and
|
|
354
|
+
// warns: only remote MCP depends on them.
|
|
355
|
+
const s = r.step("Resolving the remote-MCP URLs and IAP audience...");
|
|
356
|
+
const resolved = await resolveRemoteMcpIdentity(
|
|
357
|
+
{
|
|
358
|
+
provider: install.provider,
|
|
359
|
+
projectId: install.projectId,
|
|
360
|
+
region: install.region,
|
|
361
|
+
env: install.env,
|
|
362
|
+
domain: install.domain,
|
|
363
|
+
services,
|
|
364
|
+
},
|
|
365
|
+
runDeps,
|
|
366
|
+
);
|
|
367
|
+
if (resolved.ok) {
|
|
368
|
+
s.done(
|
|
369
|
+
`Google sign-in and remote MCP can be set up under Settings → Google sign-in (${resolved.identity.mcpPublicUrl})`,
|
|
370
|
+
);
|
|
371
|
+
if (resolved.orgDomainError) {
|
|
372
|
+
// #914 — the admin then refuses new users until Google sign-in is
|
|
373
|
+
// set up (it cannot tell who IAP's built-in sign-in admits). Not a
|
|
374
|
+
// converge failure: the Owner's sign-in setup resolves it.
|
|
375
|
+
r.warn(
|
|
376
|
+
`Could not read this project's Google organisation (${resolved.orgDomainError}). Until Google sign-in is set up under Settings → Google sign-in, the admin refuses to add users; or re-run upgrade as an account with the Organization Viewer role.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return remoteMcpIdentityEnv(resolved.identity);
|
|
380
|
+
}
|
|
381
|
+
s.warned(`Remote-MCP env not resolved: ${resolved.error}`);
|
|
382
|
+
r.warn(
|
|
383
|
+
"The upgrade continues; Settings → Google sign-in and Settings → MCP report this install as not prepared until this succeeds. Re-run upgrade once the cause is fixed.",
|
|
384
|
+
);
|
|
385
|
+
incomplete = true;
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
311
389
|
/** The admin's IAP resource: the service on gcp-firebase, the LB backend on gcp. */
|
|
312
390
|
async function adminIapResource(): Promise<IapResource | null> {
|
|
313
391
|
if (install.provider === "gcp-firebase") {
|
|
@@ -394,6 +472,54 @@ export function startConverge(
|
|
|
394
472
|
leaveUntouched.push(UPGRADER_JOB_ENV_VAR);
|
|
395
473
|
}
|
|
396
474
|
|
|
475
|
+
async function convergeJob(): Promise<void> {
|
|
476
|
+
// #819 — the job "Install update" starts for the next release that
|
|
477
|
+
// changes the cloud setup, on this release's ops image. After
|
|
478
|
+
// migrations: its database login gets the role migration 0267 creates.
|
|
479
|
+
// A failure only costs in-Caelo converges (the terminal path stays), so
|
|
480
|
+
// it warns.
|
|
481
|
+
const s = r.step("Ensuring the converge job (cloud-setup updates from Settings → Updates)...");
|
|
482
|
+
const resolved = deps.opsImageRef
|
|
483
|
+
? await resolveUpgraderTarget(
|
|
484
|
+
{
|
|
485
|
+
projectId: install.projectId,
|
|
486
|
+
region: install.region,
|
|
487
|
+
env: install.env,
|
|
488
|
+
opsImageRef: deps.opsImageRef,
|
|
489
|
+
},
|
|
490
|
+
{ run },
|
|
491
|
+
)
|
|
492
|
+
: { ok: false as const, error: "the release's caelo-ops image could not be resolved" };
|
|
493
|
+
const cj = resolved.ok
|
|
494
|
+
? await ensureConvergeJob(resolved.target, runDeps)
|
|
495
|
+
: { ok: false as const, done: [], error: resolved.error };
|
|
496
|
+
for (const step of cj.done) r.info(` ${step}`);
|
|
497
|
+
if (cj.ok) {
|
|
498
|
+
s.done("Converge job ready (updates that change the cloud setup install from the admin)");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (cj.reason === "no-provisioner") {
|
|
502
|
+
// #883 review — optional for contract 9: an install without a
|
|
503
|
+
// provisioner (installed before it existed) has nothing the job could
|
|
504
|
+
// run as, and creating one is a privileged identity no update should
|
|
505
|
+
// mint. It stays fully converged; its admin keeps no
|
|
506
|
+
// CAELO_CONVERGE_JOB and so routes cloud-setup releases to the
|
|
507
|
+
// terminal, saying so on Settings → Updates.
|
|
508
|
+
s.done("No converge job: this install has no caelo-provisioner to run it as");
|
|
509
|
+
r.info(
|
|
510
|
+
"Updates that change the cloud setup keep installing from Cloud Shell or the computer you installed from; Settings → Updates says so when one comes.",
|
|
511
|
+
);
|
|
512
|
+
leaveUntouched.push(CONVERGE_JOB_ENV_VAR);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
s.warned(`Converge job not set up: ${cj.error}`);
|
|
516
|
+
r.warn(
|
|
517
|
+
"The upgrade continues; releases that change the cloud setup keep installing from a terminal until this succeeds.",
|
|
518
|
+
);
|
|
519
|
+
incomplete = true;
|
|
520
|
+
leaveUntouched.push(CONVERGE_JOB_ENV_VAR);
|
|
521
|
+
}
|
|
522
|
+
|
|
397
523
|
async function operatorAccess(): Promise<void> {
|
|
398
524
|
// The job that keeps IAP in step with the user list (operator-access.ts).
|
|
399
525
|
// After migrations: its database user gets the role migration 0239
|
|
@@ -447,6 +573,7 @@ export function startConverge(
|
|
|
447
573
|
await mcpAccess();
|
|
448
574
|
await urlMapRedirectsAccess();
|
|
449
575
|
await auditJob();
|
|
576
|
+
await remoteMcpEdge();
|
|
450
577
|
return true;
|
|
451
578
|
},
|
|
452
579
|
|
|
@@ -454,8 +581,14 @@ export function startConverge(
|
|
|
454
581
|
async afterMigrations() {
|
|
455
582
|
if (!(await databaseLogins())) return { ok: false };
|
|
456
583
|
await upgraderJob();
|
|
584
|
+
await convergeJob();
|
|
457
585
|
await operatorAccess();
|
|
458
|
-
|
|
586
|
+
const extraEnv = await remoteMcpIdentity();
|
|
587
|
+
return {
|
|
588
|
+
ok: true,
|
|
589
|
+
leaveUntouched: [...leaveUntouched],
|
|
590
|
+
...(extraEnv ? { extraEnv } : {}),
|
|
591
|
+
};
|
|
459
592
|
},
|
|
460
593
|
async beforeRoll(slug) {
|
|
461
594
|
if (slug !== "gateway" || !publicRole) return { ok: true };
|
|
@@ -480,13 +613,18 @@ export function startConverge(
|
|
|
480
613
|
// #613 — with both services on their new revisions, drop the bindings
|
|
481
614
|
// nothing reads through any more (above all the gateway SA's access to
|
|
482
615
|
// admin_role's password). Loud on failure.
|
|
483
|
-
const retired =
|
|
616
|
+
const retired = [
|
|
617
|
+
...(await removeRetiredIamBindings(target, runDeps)),
|
|
618
|
+
// #914 — secrets nothing references any more (the renamed Google
|
|
619
|
+
// sign-in client); their IAM bindings go with them.
|
|
620
|
+
...(await deleteRetiredSecrets(target, runDeps)),
|
|
621
|
+
];
|
|
484
622
|
for (const o of retired) {
|
|
485
623
|
if (o.status === "removed") r.info(` removed: ${o.id} (${o.why})`);
|
|
486
624
|
if (o.status === "failed") {
|
|
487
625
|
incomplete = true;
|
|
488
626
|
r.error(
|
|
489
|
-
` NOT removed: ${o.id} — ${o.why}\n ${o.error ?? ""}\n Re-run upgrade with an account that may change IAM, or remove
|
|
627
|
+
` NOT removed: ${o.id} — ${o.why}\n ${o.error ?? ""}\n Re-run upgrade with an account that may change IAM and secrets, or remove it by hand.`,
|
|
490
628
|
);
|
|
491
629
|
}
|
|
492
630
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* findServingImage against a fake gcloud: the digest a revision was deployed
|
|
5
|
+
* with is the release's index digest, Cloud Run's `status.imageDigest` the
|
|
6
|
+
* platform manifest inside it (#860). Both must come back, so a recorded
|
|
7
|
+
* index digest matches what serves.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import type { GcloudResult } from "../gcloud.js";
|
|
12
|
+
import type { GcloudRunner } from "../gcloud-retry.js";
|
|
13
|
+
import { findServingImage, servingImageDigests } from "./gcp-lookup.js";
|
|
14
|
+
|
|
15
|
+
const INDEX = `sha256:${"a".repeat(64)}`;
|
|
16
|
+
const PLATFORM = `sha256:${"b".repeat(64)}`;
|
|
17
|
+
const REPO = "europe-west1-docker.pkg.dev/caelo-website/caelo-cms-images/admin";
|
|
18
|
+
|
|
19
|
+
const ok = (stdout: string): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
20
|
+
|
|
21
|
+
function gcloud(revision: GcloudResult): GcloudRunner {
|
|
22
|
+
return async (args) => {
|
|
23
|
+
if (args.includes("services")) return ok("caelo-production-admin-x-00042-abc\n");
|
|
24
|
+
return revision;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const revision = (image: string, imageDigest?: string) =>
|
|
29
|
+
ok(JSON.stringify({ spec: { containers: [{ image }] }, status: { imageDigest } }));
|
|
30
|
+
|
|
31
|
+
describe("findServingImage", () => {
|
|
32
|
+
it("returns the deployed index digest and the platform digest Cloud Run resolved", async () => {
|
|
33
|
+
const image = await findServingImage(
|
|
34
|
+
gcloud(revision(`${REPO}@${INDEX}`, `${REPO}@${PLATFORM}`)),
|
|
35
|
+
"acme",
|
|
36
|
+
"europe-west1",
|
|
37
|
+
"caelo-production-admin-x",
|
|
38
|
+
);
|
|
39
|
+
expect(image).toEqual({ deployedDigest: INDEX, resolvedDigest: PLATFORM });
|
|
40
|
+
expect(servingImageDigests(image ?? { deployedDigest: null, resolvedDigest: null })).toEqual([
|
|
41
|
+
INDEX,
|
|
42
|
+
PLATFORM,
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("a revision deployed by tag is known by its resolved digest only", async () => {
|
|
47
|
+
const image = await findServingImage(
|
|
48
|
+
gcloud(revision(`${REPO}:0.10.52`, `${REPO}@${PLATFORM}`)),
|
|
49
|
+
"acme",
|
|
50
|
+
"europe-west1",
|
|
51
|
+
"svc",
|
|
52
|
+
);
|
|
53
|
+
expect(image).toEqual({ deployedDigest: null, resolvedDigest: PLATFORM });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("one digest when both name the same image", () => {
|
|
57
|
+
expect(servingImageDigests({ deployedDigest: INDEX, resolvedDigest: INDEX })).toEqual([INDEX]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("null when no digest can be read or the output is not JSON", async () => {
|
|
61
|
+
expect(
|
|
62
|
+
await findServingImage(gcloud(revision(`${REPO}:latest`)), "acme", "europe-west1", "svc"),
|
|
63
|
+
).toBeNull();
|
|
64
|
+
expect(
|
|
65
|
+
await findServingImage(gcloud(ok("not json")), "acme", "europe-west1", "svc"),
|
|
66
|
+
).toBeNull();
|
|
67
|
+
// JSON of an unexpected shape reads as unknown instead of throwing.
|
|
68
|
+
for (const odd of ["null", "[]", '{"spec":{"containers":{"image":1}}}', '{"status":"x"}']) {
|
|
69
|
+
expect(await findServingImage(gcloud(ok(odd)), "acme", "europe-west1", "svc")).toBeNull();
|
|
70
|
+
}
|
|
71
|
+
expect(
|
|
72
|
+
await findServingImage(
|
|
73
|
+
gcloud({ ok: false, stdout: "", stderr: "denied", exitCode: 1 }),
|
|
74
|
+
"acme",
|
|
75
|
+
"europe-west1",
|
|
76
|
+
"svc",
|
|
77
|
+
),
|
|
78
|
+
).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* by a different identity in the in-cloud upgrader job (epic #696).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { z } from "zod";
|
|
10
11
|
import type { GcloudRunner } from "../gcloud-retry.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -102,16 +103,57 @@ export async function findCurrentRevision(
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
|
-
* The image
|
|
106
|
-
* template's: after a rollback the template names the
|
|
107
|
-
* traffic stays on the previous revision).
|
|
106
|
+
* The image the revision serving traffic runs, by every digest it is known
|
|
107
|
+
* by (not the service template's: after a rollback the template names the
|
|
108
|
+
* failed image while traffic stays on the previous revision).
|
|
109
|
+
*
|
|
110
|
+
* Why two digests (#860): Caelo's release images are OCI image *indexes*
|
|
111
|
+
* (buildx adds a provenance/SBOM attestation manifest even to a
|
|
112
|
+
* single-platform build), and the signed release manifest, install.json
|
|
113
|
+
* and every deploy pin the index digest. Cloud Run's `status.imageDigest`
|
|
114
|
+
* names what it resolved and pulled, the linux/amd64 *platform manifest*
|
|
115
|
+
* inside that index, so comparing it with a recorded index digest never
|
|
116
|
+
* matches. The digest the revision was deployed with
|
|
117
|
+
* (`spec.containers[0].image`, `…@sha256:<index>`) is the form everything
|
|
118
|
+
* else records; it is content-addressed, so the revision necessarily runs
|
|
119
|
+
* that index's image.
|
|
108
120
|
*/
|
|
109
|
-
export
|
|
121
|
+
export interface ServingImage {
|
|
122
|
+
/** The digest the revision was deployed with; null when deployed by tag. */
|
|
123
|
+
readonly deployedDigest: string | null;
|
|
124
|
+
/** The digest Cloud Run resolved and runs (`status.imageDigest`); null when not reported. */
|
|
125
|
+
readonly resolvedDigest: string | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Every digest `image` is known by, deployed form first. */
|
|
129
|
+
export function servingImageDigests(image: ServingImage): string[] {
|
|
130
|
+
const out: string[] = [];
|
|
131
|
+
for (const d of [image.deployedDigest, image.resolvedDigest]) {
|
|
132
|
+
if (d && !out.includes(d)) out.push(d);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const PINNED_DIGEST = /@(sha256:[0-9a-f]{64})$/;
|
|
138
|
+
|
|
139
|
+
/** The fields of `gcloud run revisions describe --format=json` the lookup reads. */
|
|
140
|
+
const RevisionJsonSchema = z.object({
|
|
141
|
+
spec: z
|
|
142
|
+
.object({ containers: z.array(z.object({ image: z.string().optional() })).optional() })
|
|
143
|
+
.optional(),
|
|
144
|
+
status: z.object({ imageDigest: z.string().optional() }).optional(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The serving revision's {@link ServingImage}, or null when the revision or
|
|
149
|
+
* any digest of it cannot be read.
|
|
150
|
+
*/
|
|
151
|
+
export async function findServingImage(
|
|
110
152
|
run: GcloudRunner,
|
|
111
153
|
projectId: string,
|
|
112
154
|
region: string,
|
|
113
155
|
serviceName: string,
|
|
114
|
-
): Promise<
|
|
156
|
+
): Promise<ServingImage | null> {
|
|
115
157
|
const revision = await findCurrentRevision(run, projectId, region, serviceName);
|
|
116
158
|
if (!revision) return null;
|
|
117
159
|
const r = await run([
|
|
@@ -123,11 +165,26 @@ export async function findServingImageDigest(
|
|
|
123
165
|
region,
|
|
124
166
|
"--project",
|
|
125
167
|
projectId,
|
|
126
|
-
"--format=
|
|
168
|
+
"--format=json",
|
|
127
169
|
]);
|
|
128
170
|
if (!r.ok) return null;
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
let json: unknown;
|
|
172
|
+
try {
|
|
173
|
+
json = JSON.parse(r.stdout);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// gcloud output is a boundary (CLAUDE.md §4): an unexpected shape (`null`,
|
|
178
|
+
// an array, a non-string image) reads as "unknown" instead of throwing.
|
|
179
|
+
const parsed = RevisionJsonSchema.safeParse(json);
|
|
180
|
+
if (!parsed.success) return null;
|
|
181
|
+
const digestOf = (ref: string | undefined): string | null =>
|
|
182
|
+
ref === undefined ? null : (PINNED_DIGEST.exec(ref.trim())?.[1] ?? null);
|
|
183
|
+
const image: ServingImage = {
|
|
184
|
+
deployedDigest: digestOf(parsed.data.spec?.containers?.[0]?.image),
|
|
185
|
+
resolvedDigest: digestOf(parsed.data.status?.imageDigest),
|
|
186
|
+
};
|
|
187
|
+
return servingImageDigests(image).length > 0 ? image : null;
|
|
131
188
|
}
|
|
132
189
|
|
|
133
190
|
/** The service's own `run.app` URL. */
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* recordRolledDigests (#860): a CLI upgrade whose records could not be
|
|
5
|
+
* written used to exit 0 with a warning, leaving install.json naming the
|
|
6
|
+
* previous release. It now reports failure (the command exits non-zero)
|
|
7
|
+
* with the exact fix, and never skips one record because another failed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import type { ImageDigests } from "../install-state.js";
|
|
12
|
+
import { type RecordRolledDeps, recordRolledDigests } from "./record-rolled.js";
|
|
13
|
+
|
|
14
|
+
const DIGESTS: ImageDigests = {
|
|
15
|
+
admin: `sha256:${"a".repeat(64)}`,
|
|
16
|
+
gateway: `sha256:${"b".repeat(64)}`,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function deps(overrides: Partial<RecordRolledDeps> = {}) {
|
|
20
|
+
const lines: { level: string; message: string }[] = [];
|
|
21
|
+
const calls: string[] = [];
|
|
22
|
+
const d: RecordRolledDeps = {
|
|
23
|
+
recordInstall: (_digests, version) => {
|
|
24
|
+
calls.push(`install ${version}`);
|
|
25
|
+
},
|
|
26
|
+
pinStack: async () => {
|
|
27
|
+
calls.push("stack");
|
|
28
|
+
return { removedOverrides: [] };
|
|
29
|
+
},
|
|
30
|
+
stackRecovery: () => " pulumi config set …",
|
|
31
|
+
installJsonLocation: "gs://acme-caelo-ops/install/install.json",
|
|
32
|
+
log: {
|
|
33
|
+
info: (message) => lines.push({ level: "info", message }),
|
|
34
|
+
warn: (message) => lines.push({ level: "warn", message }),
|
|
35
|
+
error: (message) => lines.push({ level: "error", message }),
|
|
36
|
+
},
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
return { d, lines, calls };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("recordRolledDigests", () => {
|
|
43
|
+
it("records install.json and the stack config", async () => {
|
|
44
|
+
const { d, calls, lines } = deps();
|
|
45
|
+
expect(await recordRolledDigests(DIGESTS, "0.10.53", d)).toBe(true);
|
|
46
|
+
expect(calls).toEqual(["install 0.10.53", "stack"]);
|
|
47
|
+
expect(lines.filter((l) => l.level === "error")).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("regression #860: a failed install.json write fails the upgrade, with the exact fix", async () => {
|
|
51
|
+
const { d, calls, lines } = deps({
|
|
52
|
+
recordInstall: () => {
|
|
53
|
+
throw new Error("Could not write gs://acme-caelo-ops/install/install.json: 403");
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
expect(await recordRolledDigests(DIGESTS, "0.10.53", d)).toBe(false);
|
|
57
|
+
const error = lines.find((l) => l.level === "error")?.message ?? "";
|
|
58
|
+
expect(error).toContain("gs://acme-caelo-ops/install/install.json");
|
|
59
|
+
expect(error).toContain(DIGESTS.admin);
|
|
60
|
+
expect(error).toContain('"releaseVersion": "0.10.53"');
|
|
61
|
+
// The stack config is still pinned.
|
|
62
|
+
expect(calls).toEqual(["stack"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("regression #865: with no known version, the manual fix removes the stale releaseVersion", async () => {
|
|
66
|
+
const { d, lines } = deps({
|
|
67
|
+
recordInstall: () => {
|
|
68
|
+
throw new Error("403");
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect(await recordRolledDigests(DIGESTS, null, d)).toBe(false);
|
|
72
|
+
const error = lines.find((l) => l.level === "error")?.message ?? "";
|
|
73
|
+
expect(error).toContain(DIGESTS.admin);
|
|
74
|
+
expect(error).not.toContain('"releaseVersion":');
|
|
75
|
+
expect(error).toContain('delete the "releaseVersion" field');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("with a known version, the manual fix sets it and says nothing about deleting it", async () => {
|
|
79
|
+
const { d, lines } = deps({
|
|
80
|
+
recordInstall: () => {
|
|
81
|
+
throw new Error("403");
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
await recordRolledDigests(DIGESTS, "0.10.53", d);
|
|
85
|
+
const error = lines.find((l) => l.level === "error")?.message ?? "";
|
|
86
|
+
expect(error).not.toContain("delete the");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("a failed stack-config pin fails the upgrade too, with the commands to run", async () => {
|
|
90
|
+
const { d, lines } = deps({
|
|
91
|
+
pinStack: async () => {
|
|
92
|
+
throw new Error("passphrase missing");
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(await recordRolledDigests(DIGESTS, "0.10.53", d)).toBe(false);
|
|
96
|
+
expect(lines.find((l) => l.level === "error")?.message).toContain("pulumi config set");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("an unknown release version is recorded as none, with a warning, not a failure", async () => {
|
|
100
|
+
const { d, calls, lines } = deps();
|
|
101
|
+
expect(await recordRolledDigests(DIGESTS, null, d)).toBe(true);
|
|
102
|
+
expect(calls).toEqual(["install null", "stack"]);
|
|
103
|
+
expect(lines.some((l) => l.level === "warn")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* After a CLI upgrade rolled: record the digests that now serve, so nothing
|
|
5
|
+
* re-deploys an older release — install.json (what a wizard re-run deploys
|
|
6
|
+
* and what the in-cloud upgrader judges the next update by) and the Pulumi
|
|
7
|
+
* stack config (what a plain `pulumi up` deploys).
|
|
8
|
+
*
|
|
9
|
+
* The roll already succeeded, so a failed write never undoes it, and one
|
|
10
|
+
* failing never skips the other. But it is a failed upgrade, not a warning
|
|
11
|
+
* (#860, CLAUDE.md §2 no fallbacks): a run that exits 0 while install.json
|
|
12
|
+
* still names the previous release leaves every later update judging
|
|
13
|
+
* against a stale record. So each failure prints the exact manual fix and
|
|
14
|
+
* the result makes the command exit non-zero.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ImageDigests } from "../install-state.js";
|
|
18
|
+
|
|
19
|
+
/** Where the records go; the real ones are wired in lifecycle.ts. */
|
|
20
|
+
export interface RecordRolledDeps {
|
|
21
|
+
/** Write install.json (recordImageDigests). Throws when it cannot. */
|
|
22
|
+
readonly recordInstall: (digests: ImageDigests, releaseVersion: string | null) => void;
|
|
23
|
+
/** Pin the digests in the Pulumi stack config. Throws when it cannot. */
|
|
24
|
+
readonly pinStack: (
|
|
25
|
+
digests: ImageDigests,
|
|
26
|
+
) => Promise<{ readonly removedOverrides: readonly string[] }>;
|
|
27
|
+
/** The commands that pin `digests` in the stack by hand. */
|
|
28
|
+
readonly stackRecovery: (digests: ImageDigests) => string;
|
|
29
|
+
/** Where install.json lives, for the manual fix (`gs://…/install.json` or a path). */
|
|
30
|
+
readonly installJsonLocation: string;
|
|
31
|
+
readonly log: {
|
|
32
|
+
readonly info: (message: string) => void;
|
|
33
|
+
readonly warn: (message: string) => void;
|
|
34
|
+
readonly error: (message: string) => void;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Record the rolled `digests` (and `releaseVersion`, when known).
|
|
40
|
+
*
|
|
41
|
+
* @returns whether every record was written; false means the upgrade must
|
|
42
|
+
* exit non-zero — the fix for each failure has been printed.
|
|
43
|
+
*/
|
|
44
|
+
export async function recordRolledDigests(
|
|
45
|
+
digests: ImageDigests,
|
|
46
|
+
releaseVersion: string | null,
|
|
47
|
+
deps: RecordRolledDeps,
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
const { log } = deps;
|
|
50
|
+
const reason = (e: unknown) => (e instanceof Error ? e.message.split("\n")[0] : String(e));
|
|
51
|
+
if (!releaseVersion) {
|
|
52
|
+
log.warn(
|
|
53
|
+
"Could not tell which Caelo release these images are (no version tag points at them), so install.json records none; in-product updates stay unavailable until an upgrade to a released version.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
let ok = true;
|
|
57
|
+
try {
|
|
58
|
+
deps.recordInstall(digests, releaseVersion);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
ok = false;
|
|
61
|
+
const fields = {
|
|
62
|
+
imageDigests: digests,
|
|
63
|
+
...(releaseVersion ? { releaseVersion } : {}),
|
|
64
|
+
};
|
|
65
|
+
// With no known version the fix must REMOVE the recorded one: left in
|
|
66
|
+
// place, the previous release's version would sit next to the new
|
|
67
|
+
// digests, and the upgrader (which matches records by digest only)
|
|
68
|
+
// would judge the next update's downgrade / minFromVersion against it.
|
|
69
|
+
const versionFix = releaseVersion
|
|
70
|
+
? ""
|
|
71
|
+
: '\nAlso delete the "releaseVersion" field from that file: it names the previous release, not the one that serves now.';
|
|
72
|
+
log.error(
|
|
73
|
+
`The new release serves, but its digests could not be recorded in ${deps.installJsonLocation} (${reason(e)}). Set these fields there before anything else, or a re-run of the installer deploys another release and in-product updates refuse to run:\n${JSON.stringify(fields, null, 2)}${versionFix}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const { removedOverrides } = await deps.pinStack(digests);
|
|
78
|
+
log.info("Pinned the rolled image digests in the Pulumi stack config.");
|
|
79
|
+
if (removedOverrides.length > 0) {
|
|
80
|
+
log.warn(
|
|
81
|
+
`Removed ${removedOverrides.join(", ")} from the stack config: those image overrides win over the digest pins, so a later \`pulumi up\` would have rolled back to them.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
ok = false;
|
|
86
|
+
log.error(
|
|
87
|
+
`Could not pin the rolled digests in the Pulumi stack config (${reason(e)}). Before any manual \`pulumi up\`, run:\n${deps.stackRecovery(digests)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return ok;
|
|
91
|
+
}
|