@caelo-cms/provisioning 0.10.46 → 0.10.48
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/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +122 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +291 -83
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `cms-provision upgrade` for one gcp / gcp-firebase install: converge, then
|
|
5
|
+
* app-roll (epic #696). The order, and why:
|
|
6
|
+
*
|
|
7
|
+
* 1. plan (read-only): release digests, both services, the env contract
|
|
8
|
+
* 2. verify the images' signatures
|
|
9
|
+
* 3. pre-upgrade Cloud SQL backup — the first change; if it fails, the
|
|
10
|
+
* install is exactly as it was
|
|
11
|
+
* 4. converge, the part before the roll (keyless provisioner, IAM, secrets,
|
|
12
|
+
* MCP access)
|
|
13
|
+
* 5. app-roll: health baseline, migrations, the converge steps that must
|
|
14
|
+
* follow migrations, admin + gateway rolls, deep health check, rollback
|
|
15
|
+
* on failure, recorded digests
|
|
16
|
+
* 6. converge, the part after the roll; a complete converge records the
|
|
17
|
+
* install's infrastructure contract version
|
|
18
|
+
*
|
|
19
|
+
* Every side effect goes through {@link GcpUpgradeDeps}, so the sequence runs
|
|
20
|
+
* against a fake gcloud in tests. lifecycle.ts wires the real ones.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
24
|
+
import type { ImageDigests } from "../install-state.js";
|
|
25
|
+
import type { HttpFetch } from "../runtime-secrets.js";
|
|
26
|
+
import {
|
|
27
|
+
type AppRollPlan,
|
|
28
|
+
executeAppRoll,
|
|
29
|
+
planAppRoll,
|
|
30
|
+
type ReleaseImage,
|
|
31
|
+
type RollSlug,
|
|
32
|
+
reportPlan,
|
|
33
|
+
} from "./app-roll.js";
|
|
34
|
+
import { backupFailureHelp, restoreCommand, takeSqlBackup } from "./backup.js";
|
|
35
|
+
import { startConverge } from "./converge.js";
|
|
36
|
+
import type { HealthProbe } from "./health.js";
|
|
37
|
+
import type { UpgradeReporter } from "./reporter.js";
|
|
38
|
+
|
|
39
|
+
export interface GcpUpgradeTarget {
|
|
40
|
+
readonly provider: "gcp" | "gcp-firebase";
|
|
41
|
+
readonly projectId: string;
|
|
42
|
+
readonly region: string;
|
|
43
|
+
/** Pulumi stack name (`production`). */
|
|
44
|
+
readonly env: string;
|
|
45
|
+
readonly domain: string;
|
|
46
|
+
readonly stagingHost: string;
|
|
47
|
+
readonly ownerEmail: string;
|
|
48
|
+
/** The tag rolled to: a version (`0.11.0`) or a channel tag (`latest`, `rc`). */
|
|
49
|
+
readonly targetTag: string;
|
|
50
|
+
/** The infrastructure contract version this CLI converges to (infra-contract.ts). */
|
|
51
|
+
readonly infraContractVersion: number;
|
|
52
|
+
readonly skipVerify: boolean;
|
|
53
|
+
readonly skipHealthCheck: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GcpUpgradeDeps {
|
|
57
|
+
readonly run: GcloudRunner;
|
|
58
|
+
readonly reporter: UpgradeReporter;
|
|
59
|
+
readonly sleep?: Sleep;
|
|
60
|
+
/** Cloud SQL Admin API calls (database role passwords). */
|
|
61
|
+
readonly http?: HttpFetch;
|
|
62
|
+
readonly resolveImages: (
|
|
63
|
+
tag: string,
|
|
64
|
+
) => Promise<
|
|
65
|
+
| { readonly ok: true; readonly images: Record<RollSlug, ReleaseImage> }
|
|
66
|
+
| { readonly ok: false; readonly error: string }
|
|
67
|
+
>;
|
|
68
|
+
readonly verifyImages: (images: readonly { slug: string; digest: string }[]) => Promise<boolean>;
|
|
69
|
+
readonly migrate: (
|
|
70
|
+
imageRef: string,
|
|
71
|
+
) => Promise<{ readonly ok: boolean; readonly error?: string }>;
|
|
72
|
+
readonly healthProbe: (
|
|
73
|
+
plan: AppRollPlan,
|
|
74
|
+
) => Promise<
|
|
75
|
+
| { readonly ok: true; readonly probe: HealthProbe }
|
|
76
|
+
| { readonly ok: false; readonly error: string }
|
|
77
|
+
>;
|
|
78
|
+
readonly recordDigests: (digests: ImageDigests) => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* The release's caelo-ops image (`…/ops@sha256:…`) for the upgrader job
|
|
81
|
+
* the converge sets up (#696), or null when the tag has none.
|
|
82
|
+
*/
|
|
83
|
+
readonly resolveOpsImage: (tag: string) => Promise<string | null>;
|
|
84
|
+
readonly recordInfraContractVersion: (version: number) => void;
|
|
85
|
+
/** The operator-identity converge step (keyless provisioner); see ConvergeDeps. */
|
|
86
|
+
readonly provisionerIdentity?: () => Promise<boolean>;
|
|
87
|
+
/** Today, for the backup's description. */
|
|
88
|
+
readonly now?: () => Date;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Upgrade one install; true on success. See the file comment for the order. */
|
|
92
|
+
export async function upgradeGcpInstall(
|
|
93
|
+
target: GcpUpgradeTarget,
|
|
94
|
+
deps: GcpUpgradeDeps,
|
|
95
|
+
): Promise<boolean> {
|
|
96
|
+
const { reporter: r, run } = deps;
|
|
97
|
+
const { projectId } = target;
|
|
98
|
+
|
|
99
|
+
// 1. Plan. Nothing changes until every lookup succeeded.
|
|
100
|
+
const sPre = r.step("Pre-flight: resolving services + image digests + current revisions...");
|
|
101
|
+
const images = await deps.resolveImages(target.targetTag);
|
|
102
|
+
if (!images.ok) {
|
|
103
|
+
sPre.failed("Couldn't resolve the release images");
|
|
104
|
+
r.error(images.error);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const planned = await planAppRoll(
|
|
108
|
+
{
|
|
109
|
+
provider: target.provider,
|
|
110
|
+
projectId,
|
|
111
|
+
region: target.region,
|
|
112
|
+
env: target.env,
|
|
113
|
+
domain: target.domain,
|
|
114
|
+
stagingHost: target.stagingHost,
|
|
115
|
+
},
|
|
116
|
+
images.images,
|
|
117
|
+
run,
|
|
118
|
+
);
|
|
119
|
+
if (!planned.ok) {
|
|
120
|
+
sPre.failed("Pre-flight failed");
|
|
121
|
+
r.error(planned.error);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const plan = planned.plan;
|
|
125
|
+
sPre.done(`Pre-flight ok — ${plan.services.length} services planned`);
|
|
126
|
+
reportPlan(plan, r);
|
|
127
|
+
|
|
128
|
+
// 2. P21 ship 4 — signatures against the Caelo release workflow's
|
|
129
|
+
// identity: refuses a compromised registry, a typosquat or a fork's image.
|
|
130
|
+
if (!target.skipVerify) {
|
|
131
|
+
if (!(await deps.verifyImages(plan.services))) return false;
|
|
132
|
+
} else {
|
|
133
|
+
r.warn("--skip-verify set — image signatures NOT verified.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. The first change: a failure leaves the install exactly as it was.
|
|
137
|
+
const sBak = r.step("Taking a pre-upgrade Cloud SQL backup...");
|
|
138
|
+
const backup = await takeSqlBackup(
|
|
139
|
+
run,
|
|
140
|
+
{ projectId, env: target.env },
|
|
141
|
+
`pre-upgrade to ${target.targetTag}`,
|
|
142
|
+
deps.now,
|
|
143
|
+
);
|
|
144
|
+
if (!backup.ok) {
|
|
145
|
+
sBak.failed(`Pre-upgrade backup failed: ${backup.error}`);
|
|
146
|
+
r.warn(backupFailureHelp({ projectId }));
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
sBak.done(`Pre-upgrade backup ${backup.backup.backupId} of ${backup.backup.instance} taken`);
|
|
150
|
+
|
|
151
|
+
// 4. Converge, the part before the roll. The upgrader job runs the ops
|
|
152
|
+
// image with rights over the whole app, so it is deployed only when its
|
|
153
|
+
// signature checks out (an unverified image is never set up).
|
|
154
|
+
const opsImageRef = await verifiedOpsImage(target, deps);
|
|
155
|
+
const converge = startConverge(plan, target.ownerEmail, {
|
|
156
|
+
opsImageRef,
|
|
157
|
+
run,
|
|
158
|
+
reporter: r,
|
|
159
|
+
...(deps.sleep ? { sleep: deps.sleep } : {}),
|
|
160
|
+
...(deps.http ? { http: deps.http } : {}),
|
|
161
|
+
...(deps.provisionerIdentity ? { provisionerIdentity: deps.provisionerIdentity } : {}),
|
|
162
|
+
});
|
|
163
|
+
if (!(await converge.beforeAppRoll())) return false;
|
|
164
|
+
|
|
165
|
+
// 5. App-roll, with the converge steps that sit between its own.
|
|
166
|
+
let health: HealthProbe | null = null;
|
|
167
|
+
if (!target.skipHealthCheck) {
|
|
168
|
+
const probe = await deps.healthProbe(plan);
|
|
169
|
+
if (!probe.ok) {
|
|
170
|
+
r.error(probe.error);
|
|
171
|
+
r.warn("No migrations ran and no traffic moved.");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
health = probe.probe;
|
|
175
|
+
}
|
|
176
|
+
const rolled = await executeAppRoll(
|
|
177
|
+
{
|
|
178
|
+
plan,
|
|
179
|
+
backup: backup.backup,
|
|
180
|
+
targetVersion: /^\d+\.\d+\.\d+/.test(target.targetTag) ? target.targetTag : null,
|
|
181
|
+
// Unknown until the signed release manifest names it (epic #696).
|
|
182
|
+
migrationsContract: null,
|
|
183
|
+
},
|
|
184
|
+
converge.hooks,
|
|
185
|
+
{
|
|
186
|
+
run,
|
|
187
|
+
reporter: r,
|
|
188
|
+
...(deps.sleep ? { sleep: deps.sleep } : {}),
|
|
189
|
+
migrate: deps.migrate,
|
|
190
|
+
health,
|
|
191
|
+
recordDigests: deps.recordDigests,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
if (!rolled.ok) return false;
|
|
195
|
+
r.success(`Upgrade to ${target.targetTag} complete (admin + gateway revisions serving).`);
|
|
196
|
+
r.info(
|
|
197
|
+
`Pre-upgrade backup: ${backup.backup.backupId} (restore: ${restoreCommand(backup.backup)})`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// 6. Converge, the part after the roll.
|
|
201
|
+
await converge.afterAppRoll();
|
|
202
|
+
if (converge.complete()) {
|
|
203
|
+
try {
|
|
204
|
+
deps.recordInfraContractVersion(target.infraContractVersion);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
r.warn(
|
|
207
|
+
`Could not record the infrastructure contract version (${e instanceof Error ? e.message : String(e)}); the next upgrade converges again.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
r.warn(
|
|
212
|
+
"Some converge steps did not succeed (see above); the install is not marked as converged, so in-product updates stay unavailable until a re-run of upgrade completes them.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The release's caelo-ops image for the upgrader job, signature-verified
|
|
220
|
+
* like the admin and gateway images; null (with the reason printed) when
|
|
221
|
+
* there is none, it does not verify, or verification was skipped.
|
|
222
|
+
*/
|
|
223
|
+
async function verifiedOpsImage(
|
|
224
|
+
target: GcpUpgradeTarget,
|
|
225
|
+
deps: GcpUpgradeDeps,
|
|
226
|
+
): Promise<string | null> {
|
|
227
|
+
const r = deps.reporter;
|
|
228
|
+
const ref = await deps.resolveOpsImage(target.targetTag);
|
|
229
|
+
const digest = ref ? /@(sha256:[0-9a-f]{64})$/.exec(ref)?.[1] : undefined;
|
|
230
|
+
if (!ref || !digest) {
|
|
231
|
+
r.warn(
|
|
232
|
+
`The release ${target.targetTag} has no caelo-ops image in the registry, so the upgrader job (in-product updates) cannot be set up this time.`,
|
|
233
|
+
);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (target.skipVerify) {
|
|
237
|
+
r.warn(
|
|
238
|
+
"--skip-verify set — the upgrader job is not set up or updated without a verified image.",
|
|
239
|
+
);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (!(await deps.verifyImages([{ slug: "ops", digest }]))) {
|
|
243
|
+
r.warn(
|
|
244
|
+
"The caelo-ops image's signature did not verify, so the upgrader job is not set up or updated.",
|
|
245
|
+
);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return ref;
|
|
249
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The upgrader's refusal matrix (upgrader-assess.ts), table-driven: every
|
|
5
|
+
* row is one way a request can be refused, handed to Cloud Shell, or let
|
|
6
|
+
* through.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import type { StoredUpdatePolicy } from "@caelo-cms/shared";
|
|
11
|
+
import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
|
|
12
|
+
import { assessUpgradeRequest, type InstalledState } from "./upgrader-assess.js";
|
|
13
|
+
|
|
14
|
+
const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
15
|
+
|
|
16
|
+
const MANIFEST: ReleaseManifest = {
|
|
17
|
+
manifestVersion: 1,
|
|
18
|
+
version: "0.11.0",
|
|
19
|
+
channel: "stable",
|
|
20
|
+
digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
|
|
21
|
+
minFromVersion: "0.10.0",
|
|
22
|
+
infraContractVersion: 2,
|
|
23
|
+
migrations: { contract: false, lastContractVersion: null, head: "0267_platform_updates" },
|
|
24
|
+
security: false,
|
|
25
|
+
advisories: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const POLICY: StoredUpdatePolicy = {
|
|
29
|
+
mode: "auto",
|
|
30
|
+
autoSecurity: true,
|
|
31
|
+
autoPatch: true,
|
|
32
|
+
autoMinor: true,
|
|
33
|
+
channel: "stable",
|
|
34
|
+
windowStart: null,
|
|
35
|
+
windowEnd: null,
|
|
36
|
+
windowTimezone: "UTC",
|
|
37
|
+
soakHours: 24,
|
|
38
|
+
paused: false,
|
|
39
|
+
pausedReason: null,
|
|
40
|
+
pausedAt: null,
|
|
41
|
+
updatedAt: "2026-10-13T00:00:00Z",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const INSTALLED: InstalledState = { version: "0.10.45", infraContractVersion: 2 };
|
|
45
|
+
const NOW = new Date("2026-10-13T12:00:00Z");
|
|
46
|
+
|
|
47
|
+
const verified = (m: Partial<ReleaseManifest> = {}): ReleaseManifestResult => ({
|
|
48
|
+
ok: true,
|
|
49
|
+
manifest: { ...MANIFEST, ...m },
|
|
50
|
+
});
|
|
51
|
+
const broken = (
|
|
52
|
+
code: "registry" | "signature" | "schema" | "blob-digest" | "version-mismatch" | "artifact-shape",
|
|
53
|
+
): ReleaseManifestResult => ({ ok: false, error: { code, message: `${code} problem` } });
|
|
54
|
+
|
|
55
|
+
interface Row {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly manifest?: ReleaseManifestResult;
|
|
58
|
+
readonly installed?: InstalledState;
|
|
59
|
+
readonly trigger?: "manual" | "auto";
|
|
60
|
+
readonly policy?: StoredUpdatePolicy;
|
|
61
|
+
readonly target?: string;
|
|
62
|
+
/** "proceed" or `<status>/<code>`. */
|
|
63
|
+
readonly expect: string;
|
|
64
|
+
readonly reasonContains?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ROWS: readonly Row[] = [
|
|
68
|
+
{ name: "a signed, newer, app-only release", expect: "proceed" },
|
|
69
|
+
{
|
|
70
|
+
name: "registry unreachable",
|
|
71
|
+
manifest: broken("registry"),
|
|
72
|
+
expect: "failed/registry",
|
|
73
|
+
reasonContains: "Nothing was changed",
|
|
74
|
+
},
|
|
75
|
+
{ name: "bad signature", manifest: broken("signature"), expect: "refused/signature" },
|
|
76
|
+
{ name: "tampered blob", manifest: broken("blob-digest"), expect: "refused/signature" },
|
|
77
|
+
{ name: "replayed manifest", manifest: broken("version-mismatch"), expect: "refused/signature" },
|
|
78
|
+
{ name: "not an artifact", manifest: broken("artifact-shape"), expect: "refused/signature" },
|
|
79
|
+
{
|
|
80
|
+
name: "manifest too new for this upgrader",
|
|
81
|
+
manifest: broken("schema"),
|
|
82
|
+
expect: "needs_cloud_shell/manifest-unreadable",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "installed version never recorded",
|
|
86
|
+
installed: { version: null, infraContractVersion: 2 },
|
|
87
|
+
expect: "needs_cloud_shell/install-version-unknown",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "same version",
|
|
91
|
+
installed: { version: "0.11.0", infraContractVersion: 2 },
|
|
92
|
+
expect: "refused/not-newer",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "downgrade",
|
|
96
|
+
installed: { version: "0.12.0", infraContractVersion: 2 },
|
|
97
|
+
expect: "refused/not-newer",
|
|
98
|
+
reasonContains: "never installs an older release",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "below minFromVersion",
|
|
102
|
+
manifest: verified({ minFromVersion: "0.10.50" }),
|
|
103
|
+
expect: "refused/min-from-version",
|
|
104
|
+
reasonContains: "Install v0.10.50 first",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "infrastructure contract raised",
|
|
108
|
+
installed: { version: "0.10.45", infraContractVersion: 1 },
|
|
109
|
+
expect: "needs_cloud_shell/infra-contract",
|
|
110
|
+
reasonContains: "Cloud Shell",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "never converged at all",
|
|
114
|
+
installed: { version: "0.10.45", infraContractVersion: 0 },
|
|
115
|
+
expect: "needs_cloud_shell/infra-contract",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "contract migrations, Owner's click",
|
|
119
|
+
manifest: verified({ migrations: { ...MANIFEST.migrations, contract: true } }),
|
|
120
|
+
expect: "proceed",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "contract migrations, automatic",
|
|
124
|
+
manifest: verified({ migrations: { ...MANIFEST.migrations, contract: true } }),
|
|
125
|
+
trigger: "auto",
|
|
126
|
+
expect: "refused/contract-migrations",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "skips a contract release, automatic",
|
|
130
|
+
manifest: verified({ migrations: { ...MANIFEST.migrations, lastContractVersion: "0.10.50" } }),
|
|
131
|
+
trigger: "auto",
|
|
132
|
+
expect: "refused/contract-migrations",
|
|
133
|
+
},
|
|
134
|
+
{ name: "automatic, allowed by policy", trigger: "auto", expect: "proceed" },
|
|
135
|
+
{
|
|
136
|
+
name: "automatic, policy off",
|
|
137
|
+
trigger: "auto",
|
|
138
|
+
policy: { ...POLICY, mode: "notify" },
|
|
139
|
+
expect: "refused/policy",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "automatic, paused",
|
|
143
|
+
trigger: "auto",
|
|
144
|
+
policy: { ...POLICY, paused: true },
|
|
145
|
+
expect: "refused/policy",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "automatic, minor not enabled",
|
|
149
|
+
trigger: "auto",
|
|
150
|
+
policy: { ...POLICY, autoMinor: false },
|
|
151
|
+
expect: "refused/policy",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "automatic security fix, only security enabled",
|
|
155
|
+
trigger: "auto",
|
|
156
|
+
policy: { ...POLICY, autoMinor: false, autoPatch: false },
|
|
157
|
+
manifest: verified({ security: true, advisories: ["GHSA-abcd-efgh-ijkm"] }),
|
|
158
|
+
expect: "proceed",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "automatic, outside the window",
|
|
162
|
+
trigger: "auto",
|
|
163
|
+
policy: { ...POLICY, windowStart: "02:00", windowEnd: "03:00" },
|
|
164
|
+
expect: "refused/policy",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "automatic, release candidate",
|
|
168
|
+
trigger: "auto",
|
|
169
|
+
target: "0.11.0-rc.1",
|
|
170
|
+
manifest: verified({ version: "0.11.0-rc.1", channel: "rc" }),
|
|
171
|
+
expect: "refused/policy",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
describe("assessUpgradeRequest — refusal matrix", () => {
|
|
176
|
+
it.each(ROWS.map((r) => [r.name, r] as const))("%s", (_name, row) => {
|
|
177
|
+
const d = assessUpgradeRequest({
|
|
178
|
+
request: { targetVersion: row.target ?? "0.11.0", trigger: row.trigger ?? "manual" },
|
|
179
|
+
manifest: row.manifest ?? verified(),
|
|
180
|
+
installed: row.installed ?? INSTALLED,
|
|
181
|
+
policy: row.policy ?? POLICY,
|
|
182
|
+
now: NOW,
|
|
183
|
+
});
|
|
184
|
+
const got = d.kind === "proceed" ? "proceed" : `${d.status}/${d.codes.join(",")}`;
|
|
185
|
+
expect(got).toBe(row.expect);
|
|
186
|
+
if (row.reasonContains) expect(d.kind === "stop" ? d.reason : "").toContain(row.reasonContains);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("warns the Owner about destructive migrations it lets through", () => {
|
|
190
|
+
const d = assessUpgradeRequest({
|
|
191
|
+
request: { targetVersion: "0.11.0", trigger: "manual" },
|
|
192
|
+
manifest: verified({ migrations: { ...MANIFEST.migrations, contract: true } }),
|
|
193
|
+
installed: INSTALLED,
|
|
194
|
+
policy: POLICY,
|
|
195
|
+
now: NOW,
|
|
196
|
+
});
|
|
197
|
+
expect(d.kind === "proceed" ? d.warnings.join(" ") : "").toContain("backup must be restored");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("keeps the verified manifest on a stop, for the facts the admin explains", () => {
|
|
201
|
+
const d = assessUpgradeRequest({
|
|
202
|
+
request: { targetVersion: "0.11.0", trigger: "manual" },
|
|
203
|
+
manifest: verified(),
|
|
204
|
+
installed: { version: "0.10.45", infraContractVersion: 1 },
|
|
205
|
+
policy: POLICY,
|
|
206
|
+
now: NOW,
|
|
207
|
+
});
|
|
208
|
+
expect(d.kind === "stop" && d.manifest?.infraContractVersion).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("assessUpgradeRequest — self-hosted (the host agent, #696 Phase 4c)", () => {
|
|
213
|
+
const SELF_HOSTED: InstalledState = { version: "0.10.45", infraContractVersion: null };
|
|
214
|
+
const assess = (row: {
|
|
215
|
+
installed?: InstalledState;
|
|
216
|
+
manifest?: ReleaseManifestResult;
|
|
217
|
+
trigger?: "manual" | "auto";
|
|
218
|
+
}) =>
|
|
219
|
+
assessUpgradeRequest({
|
|
220
|
+
request: { targetVersion: "0.11.0", trigger: row.trigger ?? "manual" },
|
|
221
|
+
manifest: row.manifest ?? verified(),
|
|
222
|
+
installed: row.installed ?? SELF_HOSTED,
|
|
223
|
+
policy: POLICY,
|
|
224
|
+
now: NOW,
|
|
225
|
+
manualPath: "server",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("never sends a release to Cloud Shell for its infrastructure contract", () => {
|
|
229
|
+
// The target's own CLI renders the Compose file in the same run.
|
|
230
|
+
expect(assess({ manifest: verified({ infraContractVersion: 9 }) }).kind).toBe("proceed");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("does not count the infrastructure contract as a reason to keep an automatic run manual", () => {
|
|
234
|
+
expect(assess({ manifest: verified({ infraContractVersion: 9 }), trigger: "auto" }).kind).toBe(
|
|
235
|
+
"proceed",
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("names the upgrade command on the server, not Cloud Shell, for a hand-run upgrade", () => {
|
|
240
|
+
const d = assess({ installed: { version: null, infraContractVersion: null } });
|
|
241
|
+
expect(d.kind === "stop" ? `${d.status}/${d.codes.join(",")}` : d.kind).toBe(
|
|
242
|
+
"needs_cloud_shell/install-version-unknown",
|
|
243
|
+
);
|
|
244
|
+
const reason = d.kind === "stop" ? d.reason : "";
|
|
245
|
+
expect(reason).toContain("upgrade command on the server");
|
|
246
|
+
expect(reason).not.toContain("Cloud Shell");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("keeps every other refusal: downgrade, signature, contract migrations unattended", () => {
|
|
250
|
+
const code = (d: ReturnType<typeof assess>) =>
|
|
251
|
+
d.kind === "stop" ? `${d.status}/${d.codes.join(",")}` : d.kind;
|
|
252
|
+
expect(code(assess({ installed: { version: "0.12.0", infraContractVersion: null } }))).toBe(
|
|
253
|
+
"refused/not-newer",
|
|
254
|
+
);
|
|
255
|
+
expect(code(assess({ manifest: broken("signature") }))).toBe("refused/signature");
|
|
256
|
+
expect(
|
|
257
|
+
code(
|
|
258
|
+
assess({
|
|
259
|
+
manifest: verified({ migrations: { ...MANIFEST.migrations, contract: true } }),
|
|
260
|
+
trigger: "auto",
|
|
261
|
+
}),
|
|
262
|
+
),
|
|
263
|
+
).toBe("refused/contract-migrations");
|
|
264
|
+
});
|
|
265
|
+
});
|