@caelo-cms/provisioning 0.10.53 → 0.10.57
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 +272 -128
- package/dist/audit-job-setup.d.ts +69 -0
- package/dist/audit-job-setup.d.ts.map +1 -0
- package/dist/audit-job-setup.js +317 -0
- package/dist/audit-job-setup.js.map +1 -0
- package/dist/caddy.d.ts +9 -0
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +38 -0
- 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 +36 -9
- 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 +25 -1
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +36 -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 +28 -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.map +1 -1
- package/dist/operator-access.js +13 -3
- package/dist/operator-access.js.map +1 -1
- package/dist/private-file.d.ts +26 -0
- package/dist/private-file.d.ts.map +1 -0
- package/dist/private-file.js +49 -0
- package/dist/private-file.js.map +1 -0
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +8 -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 +117 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +379 -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 +175 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +183 -3
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +38 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +74 -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/tar-entries.d.ts.map +1 -1
- package/dist/tar-entries.js +84 -9
- package/dist/tar-entries.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 +88 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/audit-job-roll.d.ts +38 -0
- package/dist/upgrade/audit-job-roll.d.ts.map +1 -0
- package/dist/upgrade/audit-job-roll.js +43 -0
- package/dist/upgrade/audit-job-roll.js.map +1 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +141 -8
- 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 +132 -14
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +14 -6
- 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 +7 -0
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +130 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/run.test.ts +15 -1
- package/src/audit-job-setup.test.ts +234 -0
- package/src/audit-job-setup.ts +360 -0
- package/src/caddy-redirects.docker.test.ts +19 -6
- package/src/caddy.test.ts +30 -0
- package/src/caddy.ts +43 -0
- package/src/cli-init-validation.test.ts +44 -0
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +40 -8
- 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 +43 -1
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +7 -4
- package/src/infra-contract.ts +28 -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 +14 -0
- package/src/operator-access.ts +13 -3
- package/src/private-file.test.ts +80 -0
- package/src/private-file.ts +50 -0
- package/src/release-contract.ts +8 -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 +517 -0
- package/src/remote-mcp.ts +460 -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 +34 -2
- package/src/stack-contract.ts +305 -3
- package/src/stack-converge-secrets.test.ts +3 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +101 -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/tar-entries.test.ts +149 -7
- package/src/tar-entries.ts +90 -9
- package/src/upgrade/app-roll.ts +135 -21
- package/src/upgrade/audit-job-roll.ts +68 -0
- package/src/upgrade/converge.ts +170 -7
- 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 +279 -5
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +93 -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 +587 -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 +4 -3
- package/src/upgrader-setup.ts +137 -13
- package/src/wizards/gcp-cost.ts +15 -6
- package/src/wizards/gcp-pulumi.ts +7 -0
- package/src/wizards/gcp.ts +162 -4
- package/stacks/aws/index.ts +18 -0
- package/stacks/azure/index.ts +18 -0
- package/stacks/gcp/index.ts +59 -7
- package/stacks/gcp-firebase/index.ts +50 -6
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The converge job (#819): installs a signed Caelo release that changes the
|
|
5
|
+
* install's cloud setup (raises the infrastructure contract), from inside
|
|
6
|
+
* the install's own project, started by the Owner's Update + password in
|
|
7
|
+
* Settings → Updates. It runs as the install's `caelo-provisioner` account
|
|
8
|
+
* (converge-job-setup.ts has the trust boundary); the upgrader job, which
|
|
9
|
+
* only rolls images, keeps every other release.
|
|
10
|
+
*
|
|
11
|
+
* One job, two stages, told apart by the release this code belongs to:
|
|
12
|
+
*
|
|
13
|
+
* Stage 1 — the job still runs the ops image of the INSTALLED release.
|
|
14
|
+
* It claims the request, reads install.json from the ops bucket,
|
|
15
|
+
* fetches the target's release manifest from the FIXED registry and
|
|
16
|
+
* verifies its signature (release-manifest.ts), and decides
|
|
17
|
+
* (upgrader-assess.ts, `route: "converge"`): no downgrade, nothing
|
|
18
|
+
* below minFromVersion, no automatic converge. Then it verifies the
|
|
19
|
+
* target's ops image, points this job at it BY DIGEST and starts a new
|
|
20
|
+
* execution, handing the request over to it. It changes nothing else:
|
|
21
|
+
* the converge logic always comes from the release it installs.
|
|
22
|
+
*
|
|
23
|
+
* Stage 2 — the job runs the TARGET release's ops image (its own
|
|
24
|
+
* version is the request's). It verifies the manifest again, checks
|
|
25
|
+
* that the image it runs is the one the manifest signs, takes the
|
|
26
|
+
* install's state lock (#815; the upgrader job and the terminal
|
|
27
|
+
* `upgrade` take the same one, so they exclude each other), and runs
|
|
28
|
+
* the very upgrade the terminal runs (upgrade/upgrade.ts): backup →
|
|
29
|
+
* converge + app-roll → deep health check, rollback on failure →
|
|
30
|
+
* install.json + the infrastructure contract → the request's end.
|
|
31
|
+
*
|
|
32
|
+
* Resume. Every stage-2 fact a successor must not recompute (the backup,
|
|
33
|
+
* the revisions that served before, the release it judges from) is in the
|
|
34
|
+
* checkpoint (converge-checkpoint.ts). A killed execution leaves its
|
|
35
|
+
* request `running`; starting the job again (the page's "Continue", or
|
|
36
|
+
* any new request for the same release) adopts it once Cloud Run says the
|
|
37
|
+
* old execution has ended, and resumes from the checkpoint.
|
|
38
|
+
*
|
|
39
|
+
* Inputs. The job takes no argument and no env: the request row (a
|
|
40
|
+
* version), install.json in the ops bucket (which the admin cannot write)
|
|
41
|
+
* and the signed manifest decide everything. All side effects come in
|
|
42
|
+
* through {@link ConvergeJobDeps}, so the run is tested against fakes;
|
|
43
|
+
* converge-job-gcp.ts wires the real ones and `@caelo-cms/platform-updater`
|
|
44
|
+
* the database (`caelo-ops converge-job`).
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { ConvergeCheckpoint } from "./converge-checkpoint.js";
|
|
48
|
+
import { decideInstallJob, type ExecutionState } from "./install-job.js";
|
|
49
|
+
import type { InstallMetadata } from "./install-state.js";
|
|
50
|
+
import {
|
|
51
|
+
compareSemver,
|
|
52
|
+
type ReleaseManifest,
|
|
53
|
+
type ReleaseManifestResult,
|
|
54
|
+
} from "./release-manifest.js";
|
|
55
|
+
import { StateLockHeldError } from "./state-store.js";
|
|
56
|
+
import type { UpgradeCheckpoint } from "./upgrade/upgrade.js";
|
|
57
|
+
import { assessUpgradeRequest } from "./upgrader-assess.js";
|
|
58
|
+
import {
|
|
59
|
+
type ClaimedUpdate,
|
|
60
|
+
recordUpdateEnd,
|
|
61
|
+
type UpdateEnd,
|
|
62
|
+
type UpdateEndStatus,
|
|
63
|
+
type UpdateQueue,
|
|
64
|
+
type UpgraderRunResult,
|
|
65
|
+
} from "./upgrader-job.js";
|
|
66
|
+
import { type EventReporter, eventReporter, type JobLog } from "./upgrader-reporter.js";
|
|
67
|
+
|
|
68
|
+
/** What the converge claim found. */
|
|
69
|
+
export interface ConvergeClaim {
|
|
70
|
+
readonly request: ClaimedUpdate;
|
|
71
|
+
/**
|
|
72
|
+
* The request was already running: stage 1 handed it to this run, or a
|
|
73
|
+
* run that died left it. Fresh claims (pending → running) are false.
|
|
74
|
+
*/
|
|
75
|
+
readonly resume: boolean;
|
|
76
|
+
/** The execution the request names now (after a fresh claim: this one). */
|
|
77
|
+
readonly execution: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The converge job's view of the database (the platform_updater role). */
|
|
81
|
+
export interface ConvergeQueue extends Omit<UpdateQueue, "claim"> {
|
|
82
|
+
/**
|
|
83
|
+
* End runs that died long ago (stale), then return the running converge
|
|
84
|
+
* request, or claim the oldest pending one.
|
|
85
|
+
*/
|
|
86
|
+
claimConverge(execution: string | null): Promise<ConvergeClaim | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Point running request `requestId` at execution `to` — or at none
|
|
89
|
+
* (`null`: released for the next run to adopt) — only while it still
|
|
90
|
+
* names `from`. Resolves false when another run got there first.
|
|
91
|
+
*/
|
|
92
|
+
handover(requestId: string, from: string | null, to: string | null): Promise<boolean>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** How stage 2's upgrade ended (upgrade/upgrade.ts, seen from the job). */
|
|
96
|
+
export type ConvergeUpgradeOutcome =
|
|
97
|
+
| {
|
|
98
|
+
readonly ok: true;
|
|
99
|
+
/** Every converge step succeeded and the new contract is recorded. */
|
|
100
|
+
readonly converged: boolean;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
readonly ok: false;
|
|
104
|
+
/** Traffic went back to the revisions that served before. */
|
|
105
|
+
readonly rolledBack: boolean;
|
|
106
|
+
/** The new release serves but install.json could not record it (#860). */
|
|
107
|
+
readonly unrecorded: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export interface ConvergeJobDeps {
|
|
111
|
+
readonly queue: ConvergeQueue;
|
|
112
|
+
/** From the metadata server — never from env or argv. */
|
|
113
|
+
readonly projectId: string;
|
|
114
|
+
/** This Cloud Run execution; null outside Cloud Run. */
|
|
115
|
+
readonly execution: string | null;
|
|
116
|
+
/** The Caelo release this code belongs to (CAELO_VERSION of the image). */
|
|
117
|
+
readonly ownVersion: string;
|
|
118
|
+
/** fetchVerifiedReleaseManifest against the fixed registry. */
|
|
119
|
+
readonly fetchManifest: (version: string) => Promise<ReleaseManifestResult>;
|
|
120
|
+
/** install.json as the ops bucket holds it NOW (null when absent) — never cached. */
|
|
121
|
+
readonly readInstall: () => InstallMetadata | null;
|
|
122
|
+
/** Verify the cosign signature of the release image `slug@digest`. */
|
|
123
|
+
readonly verifyImage: (slug: "ops", digest: string) => Promise<boolean>;
|
|
124
|
+
/** The digest of the image this execution runs (from Cloud Run); null when unreadable. */
|
|
125
|
+
readonly ownImageDigest: () => Promise<string | null>;
|
|
126
|
+
/** Point this job at the ops image with `digest` (its identity, command and args stay). */
|
|
127
|
+
readonly pointJobAt: (
|
|
128
|
+
digest: string,
|
|
129
|
+
) => Promise<{ readonly ok: true } | { readonly ok: false; readonly error: string }>;
|
|
130
|
+
/** Start a new execution of this job; returns its name. */
|
|
131
|
+
readonly startJob: () => Promise<
|
|
132
|
+
| { readonly ok: true; readonly execution: string }
|
|
133
|
+
| { readonly ok: false; readonly error: string }
|
|
134
|
+
>;
|
|
135
|
+
/** Where an execution of this job stands (Cloud Run). */
|
|
136
|
+
readonly executionState: (execution: string) => Promise<ExecutionState>;
|
|
137
|
+
/**
|
|
138
|
+
* Take the install's state lock (#815); returns its release. Throws
|
|
139
|
+
* StateLockHeldError when a live run holds it, any other error when it
|
|
140
|
+
* cannot be taken or judged.
|
|
141
|
+
*/
|
|
142
|
+
readonly lockState: (onBreak: (message: string) => void) => () => void;
|
|
143
|
+
readonly readCheckpoint: () => ConvergeCheckpoint | null;
|
|
144
|
+
readonly writeCheckpoint: (checkpoint: ConvergeCheckpoint) => void;
|
|
145
|
+
/** Stage 2: the terminal's upgrade (upgrade/upgrade.ts) to `manifest`, as this account. */
|
|
146
|
+
readonly upgrade: (input: {
|
|
147
|
+
readonly manifest: ReleaseManifest;
|
|
148
|
+
readonly reporter: EventReporter;
|
|
149
|
+
readonly resume: UpgradeCheckpoint | undefined;
|
|
150
|
+
readonly onCheckpoint: (checkpoint: UpgradeCheckpoint) => void;
|
|
151
|
+
}) => Promise<ConvergeUpgradeOutcome>;
|
|
152
|
+
readonly now: () => Date;
|
|
153
|
+
readonly log: JobLog;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Run the converge job once. Never throws; see the file comment. */
|
|
157
|
+
export async function runConvergeJob(deps: ConvergeJobDeps): Promise<UpgraderRunResult> {
|
|
158
|
+
const { queue, log } = deps;
|
|
159
|
+
let claim: ConvergeClaim | null;
|
|
160
|
+
try {
|
|
161
|
+
claim = await queue.claimConverge(deps.execution);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
log({ severity: "ERROR", message: `could not read update requests: ${errorText(e)}` });
|
|
164
|
+
return { exitCode: 1, requestId: null, status: null };
|
|
165
|
+
}
|
|
166
|
+
if (!claim) {
|
|
167
|
+
log({ severity: "INFO", message: "no converge request to run; nothing to do" });
|
|
168
|
+
return { exitCode: 0, requestId: null, status: null };
|
|
169
|
+
}
|
|
170
|
+
const request = claim.request;
|
|
171
|
+
const r = eventReporter(
|
|
172
|
+
(phase, level, message) => queue.event(request.id, phase, level, message),
|
|
173
|
+
log,
|
|
174
|
+
request.id,
|
|
175
|
+
);
|
|
176
|
+
/** This run leaves the request to another (live) run: no end, no events. */
|
|
177
|
+
const leave = (message: string): UpgraderRunResult => {
|
|
178
|
+
log({ severity: "INFO", message, requestId: request.id });
|
|
179
|
+
return { exitCode: 0, requestId: request.id, status: null };
|
|
180
|
+
};
|
|
181
|
+
let releaseLock: (() => void) | null = null;
|
|
182
|
+
const unlock = () => {
|
|
183
|
+
if (!releaseLock) return;
|
|
184
|
+
try {
|
|
185
|
+
releaseLock();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
r.warn(errorText(e));
|
|
188
|
+
}
|
|
189
|
+
releaseLock = null;
|
|
190
|
+
};
|
|
191
|
+
const finish = async (end: UpdateEnd): Promise<UpgraderRunResult> => {
|
|
192
|
+
r.phase("finish");
|
|
193
|
+
if (end.status === "succeeded") r.success(`Caelo ${request.targetVersion} is installed.`);
|
|
194
|
+
else r.error(end.reason ?? end.status);
|
|
195
|
+
await r.flush();
|
|
196
|
+
unlock();
|
|
197
|
+
const recorded = await recordUpdateEnd(queue, request.id, end, log);
|
|
198
|
+
const failed = end.status === "failed" || end.status === "rolled_back" || !recorded;
|
|
199
|
+
return { exitCode: failed ? 1 : 0, requestId: request.id, status: end.status };
|
|
200
|
+
};
|
|
201
|
+
const stopWith = (status: UpdateEndStatus, codes: readonly string[], reason: string) =>
|
|
202
|
+
finish({
|
|
203
|
+
status,
|
|
204
|
+
reason,
|
|
205
|
+
codes,
|
|
206
|
+
pauseAutoUpdates:
|
|
207
|
+
request.trigger === "auto" && (status === "failed" || status === "rolled_back"),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// A request another execution started: adopt it only once that one
|
|
212
|
+
// has ended — two converges at once must never happen.
|
|
213
|
+
if (claim.resume && claim.execution !== deps.execution) {
|
|
214
|
+
if (!deps.execution) {
|
|
215
|
+
return leave("a converge request is running; only a Cloud Run execution may adopt it");
|
|
216
|
+
}
|
|
217
|
+
if (claim.execution) {
|
|
218
|
+
const live = await deps.executionState(claim.execution);
|
|
219
|
+
if (live === "running") {
|
|
220
|
+
return leave(`execution ${claim.execution} is still installing; this run does nothing`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!(await queue.handover(request.id, claim.execution, deps.execution))) {
|
|
224
|
+
return leave("another run adopted the request first; this run does nothing");
|
|
225
|
+
}
|
|
226
|
+
r.phase("start");
|
|
227
|
+
r.info(
|
|
228
|
+
`Continuing the update to ${request.targetVersion} (run ${deps.execution}; the previous run${claim.execution ? ` ${claim.execution}` : ""} stopped before it finished).`,
|
|
229
|
+
);
|
|
230
|
+
} else if (!claim.resume) {
|
|
231
|
+
r.phase("start");
|
|
232
|
+
r.info(
|
|
233
|
+
`Converge job started${deps.execution ? ` (run ${deps.execution})` : ""}: installing Caelo ${request.targetVersion}, including the changes it makes to this install's cloud setup.`,
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
r.phase("start");
|
|
237
|
+
r.info(`Caelo ${deps.ownVersion}'s own installer took over (run ${deps.execution}).`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// What the install runs — from its own state, never from the admin.
|
|
241
|
+
r.phase("verify");
|
|
242
|
+
const meta = deps.readInstall();
|
|
243
|
+
if (!meta || meta.projectId !== deps.projectId) {
|
|
244
|
+
return stopWith(
|
|
245
|
+
"needs_cloud_shell",
|
|
246
|
+
["install-state"],
|
|
247
|
+
"The install's records (install.json) are not in its own cloud project, so the converge job cannot tell what it runs. Run the upgrade once from a terminal (Settings → Updates shows how); it moves them there.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (meta.provider !== "gcp" && meta.provider !== "gcp-firebase") {
|
|
251
|
+
return stopWith(
|
|
252
|
+
"refused",
|
|
253
|
+
["provider"],
|
|
254
|
+
`The converge job only updates Google Cloud installs; this one is ${meta.provider}.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const checkpoint = deps.readCheckpoint();
|
|
258
|
+
const mine = checkpoint !== null && checkpoint.targetVersion === request.targetVersion;
|
|
259
|
+
// An interrupted run may already have recorded the new release in
|
|
260
|
+
// install.json; it is judged from the release it started from.
|
|
261
|
+
const fromVersion =
|
|
262
|
+
mine && checkpoint.state !== "succeeded"
|
|
263
|
+
? checkpoint.fromVersion
|
|
264
|
+
: (meta.releaseVersion ?? null);
|
|
265
|
+
if (mine && checkpoint.state === "succeeded" && meta.releaseVersion === request.targetVersion) {
|
|
266
|
+
r.info(`An earlier run already finished installing ${request.targetVersion}.`);
|
|
267
|
+
return finish({ status: "succeeded", reason: null, codes: [], pauseAutoUpdates: false });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const manifestResult = await deps.fetchManifest(request.targetVersion);
|
|
271
|
+
const decision = assessUpgradeRequest({
|
|
272
|
+
request,
|
|
273
|
+
manifest: manifestResult,
|
|
274
|
+
installed: { version: fromVersion, infraContractVersion: meta.infraContractVersion ?? 0 },
|
|
275
|
+
policy: request.policy,
|
|
276
|
+
now: deps.now(),
|
|
277
|
+
route: "converge",
|
|
278
|
+
});
|
|
279
|
+
if (decision.manifest) {
|
|
280
|
+
r.success(
|
|
281
|
+
`Release information of ${decision.manifest.version} verified: signed by Caelo's release workflow.`,
|
|
282
|
+
);
|
|
283
|
+
await queue.record(request.id, {
|
|
284
|
+
manifestSecurity: decision.manifest.security,
|
|
285
|
+
contractMigrations: decision.manifest.migrations.contract,
|
|
286
|
+
infraContractRequired: decision.manifest.infraContractVersion,
|
|
287
|
+
infraContractInstalled: meta.infraContractVersion ?? 0,
|
|
288
|
+
adminDigest: decision.manifest.digests.admin,
|
|
289
|
+
gatewayDigest: decision.manifest.digests.gateway,
|
|
290
|
+
opsDigest: decision.manifest.digests.ops,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
r.phase("assess");
|
|
294
|
+
if (decision.kind === "stop") {
|
|
295
|
+
return stopWith(decision.status, decision.codes, decision.reason);
|
|
296
|
+
}
|
|
297
|
+
for (const w of decision.warnings) r.warn(w);
|
|
298
|
+
const m = decision.manifest;
|
|
299
|
+
|
|
300
|
+
if (compareSemver(deps.ownVersion, m.version) !== 0) {
|
|
301
|
+
return await handOver(m);
|
|
302
|
+
}
|
|
303
|
+
return await convergeHere(m, fromVersion ?? (meta.releaseVersion as string), checkpoint, meta);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return stopWith(
|
|
306
|
+
"failed",
|
|
307
|
+
["internal"],
|
|
308
|
+
`The converge job stopped unexpectedly: ${errorText(e)}. If the site misbehaves, check Settings → Updates and the job's log.`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Stage 1: hand the request to the target release's own signed installer. */
|
|
313
|
+
async function handOver(m: ReleaseManifest): Promise<UpgraderRunResult> {
|
|
314
|
+
r.info(
|
|
315
|
+
`This installer belongs to Caelo ${deps.ownVersion}; handing over to the installer of ${m.version} itself, so the cloud setup is changed by the release that needs it.`,
|
|
316
|
+
);
|
|
317
|
+
if (!(await deps.verifyImage("ops", m.digests.ops))) {
|
|
318
|
+
return stopWith(
|
|
319
|
+
"refused",
|
|
320
|
+
["signature"],
|
|
321
|
+
`The installer image of ${m.version} is not signed by Caelo's release workflow, so nothing was changed.`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
// Under the lock (the terminal `upgrade` redeploys this job too), and
|
|
325
|
+
// released before the new run starts: it takes the lock itself.
|
|
326
|
+
const locked = lockOrBlock(false);
|
|
327
|
+
if (locked) return stopWith(locked.status, locked.codes, locked.reason);
|
|
328
|
+
const pointed = await deps.pointJobAt(m.digests.ops);
|
|
329
|
+
unlock();
|
|
330
|
+
if (!pointed.ok) {
|
|
331
|
+
return stopWith(
|
|
332
|
+
"failed",
|
|
333
|
+
["handover"],
|
|
334
|
+
`Nothing was changed: the installer of ${m.version} could not be prepared (${pointed.error}). Try again in a moment.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
// Release the request BEFORE the new run exists: it adopts a request
|
|
338
|
+
// that names no run at once, while a run still named (this one, alive
|
|
339
|
+
// for a moment longer) would make it step back.
|
|
340
|
+
if (!(await queue.handover(request.id, deps.execution, null))) {
|
|
341
|
+
return leave("the request moved on before the hand-over; this run does nothing");
|
|
342
|
+
}
|
|
343
|
+
const started = await deps.startJob();
|
|
344
|
+
if (!started.ok) {
|
|
345
|
+
return stopWith(
|
|
346
|
+
"failed",
|
|
347
|
+
["handover"],
|
|
348
|
+
`Nothing was changed: the installer of ${m.version} could not be started (${started.error}). Try again in a moment.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
r.info(`The installer of ${m.version} runs now (run ${started.execution}).`);
|
|
352
|
+
await r.flush();
|
|
353
|
+
log({
|
|
354
|
+
severity: "INFO",
|
|
355
|
+
message: `handed over to ${started.execution}`,
|
|
356
|
+
requestId: request.id,
|
|
357
|
+
});
|
|
358
|
+
return { exitCode: 0, requestId: request.id, status: null };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Stage 2: this image is the release's own installer; converge + roll. */
|
|
362
|
+
async function convergeHere(
|
|
363
|
+
m: ReleaseManifest,
|
|
364
|
+
fromVersion: string,
|
|
365
|
+
checkpoint: ConvergeCheckpoint | null,
|
|
366
|
+
judged: InstallMetadata,
|
|
367
|
+
): Promise<UpgraderRunResult> {
|
|
368
|
+
const installId = judged.installId;
|
|
369
|
+
const own = await deps.ownImageDigest();
|
|
370
|
+
if (own !== m.digests.ops) {
|
|
371
|
+
return stopWith(
|
|
372
|
+
"refused",
|
|
373
|
+
["ops-image"],
|
|
374
|
+
`Nothing was changed: this run's installer image (${own ?? "unknown"}) is not the one Caelo's release workflow signed for ${m.version} (${m.digests.ops}). Start the update again from Settings → Updates.`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
// The checkpoint's run, if it is not this one, must have ended.
|
|
378
|
+
const live =
|
|
379
|
+
checkpoint && checkpoint.execution !== deps.execution
|
|
380
|
+
? await deps.executionState(checkpoint.execution)
|
|
381
|
+
: null;
|
|
382
|
+
const decided = decideInstallJob(checkpoint, live);
|
|
383
|
+
if (decided.kind === "attach") {
|
|
384
|
+
return leave(
|
|
385
|
+
`execution ${decided.status.execution} is still converging; this run does nothing`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const resumeFrom =
|
|
389
|
+
checkpoint && checkpoint.targetVersion === m.version && checkpoint.state !== "succeeded"
|
|
390
|
+
? checkpoint
|
|
391
|
+
: null;
|
|
392
|
+
|
|
393
|
+
const changed = resumeFrom?.backup !== undefined;
|
|
394
|
+
const locked = lockOrBlock(changed);
|
|
395
|
+
if (locked) return stopWith(locked.status, locked.codes, locked.reason);
|
|
396
|
+
// #815 — everything above was judged against install.json read without
|
|
397
|
+
// the lock; another install operation (the upgrader job, a terminal
|
|
398
|
+
// upgrade) may have changed it since. Never converge on a stale view.
|
|
399
|
+
const stale = recordsChangedSince(judged, changed);
|
|
400
|
+
if (stale) return stopWith(stale.status, stale.codes, stale.reason);
|
|
401
|
+
|
|
402
|
+
const startedAt = deps.now().toISOString();
|
|
403
|
+
let current: ConvergeCheckpoint = {
|
|
404
|
+
statusVersion: 1,
|
|
405
|
+
installId,
|
|
406
|
+
execution: deps.execution ?? "local-run",
|
|
407
|
+
state: "running",
|
|
408
|
+
phase: "converge",
|
|
409
|
+
caeloVersion: deps.ownVersion,
|
|
410
|
+
startedAt: resumeFrom?.startedAt ?? startedAt,
|
|
411
|
+
updatedAt: startedAt,
|
|
412
|
+
targetVersion: m.version,
|
|
413
|
+
fromVersion,
|
|
414
|
+
requestId: request.id,
|
|
415
|
+
...(resumeFrom?.backup ? { backup: resumeFrom.backup } : {}),
|
|
416
|
+
...(resumeFrom?.priorRevisions ? { priorRevisions: resumeFrom.priorRevisions } : {}),
|
|
417
|
+
};
|
|
418
|
+
const save = (patch: Partial<ConvergeCheckpoint>) => {
|
|
419
|
+
current = { ...current, ...patch, updatedAt: deps.now().toISOString() };
|
|
420
|
+
deps.writeCheckpoint(current);
|
|
421
|
+
};
|
|
422
|
+
// Written before any change: a run killed from here on is resumable.
|
|
423
|
+
save({});
|
|
424
|
+
|
|
425
|
+
r.phase("converge");
|
|
426
|
+
const outcome = await deps.upgrade({
|
|
427
|
+
manifest: m,
|
|
428
|
+
reporter: r,
|
|
429
|
+
resume: resumeFrom
|
|
430
|
+
? {
|
|
431
|
+
...(resumeFrom.backup ? { backup: resumeFrom.backup } : {}),
|
|
432
|
+
...(resumeFrom.priorRevisions ? { priorRevisions: resumeFrom.priorRevisions } : {}),
|
|
433
|
+
}
|
|
434
|
+
: undefined,
|
|
435
|
+
onCheckpoint: (c) => {
|
|
436
|
+
if (c.backup) {
|
|
437
|
+
save({ backup: c.backup });
|
|
438
|
+
queue.record(request.id, { backupId: c.backup.backupId }).catch((e: unknown) =>
|
|
439
|
+
log({
|
|
440
|
+
severity: "WARNING",
|
|
441
|
+
message: `could not record the backup id on the request: ${errorText(e)}`,
|
|
442
|
+
requestId: request.id,
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (c.priorRevisions) save({ priorRevisions: { ...c.priorRevisions } });
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (outcome.ok) {
|
|
451
|
+
save({ state: "succeeded", phase: "Done" });
|
|
452
|
+
if (!outcome.converged) {
|
|
453
|
+
r.warn(
|
|
454
|
+
"The new release serves, but some cloud-setup steps did not finish (see above); the next update repeats them.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return finish({ status: "succeeded", reason: null, codes: [], pauseAutoUpdates: false });
|
|
458
|
+
}
|
|
459
|
+
save({ state: "failed", phase: outcome.rolledBack ? "rolled back" : "stopped" });
|
|
460
|
+
if (outcome.unrecorded) {
|
|
461
|
+
return stopWith(
|
|
462
|
+
"failed",
|
|
463
|
+
["record"],
|
|
464
|
+
`Caelo ${m.version} serves and passed its checks, but the install's records (install.json) could not be updated, so they still name the previous release. Start the update again from Settings → Updates; it records them.`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (outcome.rolledBack) {
|
|
468
|
+
return stopWith(
|
|
469
|
+
"rolled_back",
|
|
470
|
+
["health"],
|
|
471
|
+
`Caelo ${m.version} did not pass its checks, so the previous release serves again. The timeline above says which step failed.`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return stopWith(
|
|
475
|
+
"failed",
|
|
476
|
+
["converge"],
|
|
477
|
+
`The update to ${m.version} stopped before it finished (see the timeline above); the previous release keeps serving. Fix what it names and start the update again; it continues where it stopped.`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Whether install.json, re-read under the lock, is still what the run was
|
|
483
|
+
* judged on; how the request ends otherwise.
|
|
484
|
+
*/
|
|
485
|
+
function recordsChangedSince(
|
|
486
|
+
judged: InstallMetadata,
|
|
487
|
+
changed: boolean,
|
|
488
|
+
): { status: UpdateEndStatus; codes: string[]; reason: string } | null {
|
|
489
|
+
const before = changed
|
|
490
|
+
? "This update already started changing the site; "
|
|
491
|
+
: "Nothing was changed: ";
|
|
492
|
+
let now: InstallMetadata | null;
|
|
493
|
+
try {
|
|
494
|
+
now = deps.readInstall();
|
|
495
|
+
} catch (e) {
|
|
496
|
+
return {
|
|
497
|
+
status: "failed",
|
|
498
|
+
codes: ["install-state-read"],
|
|
499
|
+
reason: `${before}the install's records (install.json) could not be read again once the lock was held (${errorText(e)}). Start the update again; it continues where it stopped.`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (JSON.stringify(now) === JSON.stringify(judged)) return null;
|
|
503
|
+
return {
|
|
504
|
+
status: "failed",
|
|
505
|
+
codes: ["install-state-changed"],
|
|
506
|
+
reason: `${before}another install operation changed the install's records (install.json) while this update was being checked. Start the update again from Settings → Updates; it continues where it stopped.`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Take the state lock, or say how the request ends instead. `changed`:
|
|
512
|
+
* an earlier run of this update already changed something.
|
|
513
|
+
*/
|
|
514
|
+
function lockOrBlock(
|
|
515
|
+
changed: boolean,
|
|
516
|
+
): { status: UpdateEndStatus; codes: string[]; reason: string } | null {
|
|
517
|
+
const before = changed
|
|
518
|
+
? "This update already started changing the site; "
|
|
519
|
+
: "Nothing was changed: ";
|
|
520
|
+
try {
|
|
521
|
+
releaseLock = deps.lockState((message) => r.info(message));
|
|
522
|
+
return null;
|
|
523
|
+
} catch (e) {
|
|
524
|
+
if (e instanceof StateLockHeldError) {
|
|
525
|
+
return {
|
|
526
|
+
status: "failed",
|
|
527
|
+
codes: ["state-locked"],
|
|
528
|
+
reason: `${before}another install operation is running on this site (${errorText(e)}). Start the update again when it has finished; it continues where it stopped.`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
status: "needs_cloud_shell",
|
|
533
|
+
codes: ["state-access"],
|
|
534
|
+
reason: `${before}the converge job could not take the install's state lock (${errorText(e)}). Run the upgrade once from a terminal (Settings → Updates shows how); it repairs the job's access.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function errorText(e: unknown): string {
|
|
541
|
+
return e instanceof Error ? e.message : String(e);
|
|
542
|
+
}
|
|
@@ -43,7 +43,14 @@ const ADMIN_ACCESS = /cms_admin|admin_role|gateway_role|ADMIN_DATABASE|GATEWAY_D
|
|
|
43
43
|
const ADMIN_SECRETS = ["postgres-password", "gateway-role-password"];
|
|
44
44
|
|
|
45
45
|
/** The secrets the gateway reads on GCP: its own login and the staging gate key. */
|
|
46
|
-
const GCP_GATEWAY_SECRETS = [
|
|
46
|
+
const GCP_GATEWAY_SECRETS = [
|
|
47
|
+
"public-role-password",
|
|
48
|
+
"staging-gate-secret",
|
|
49
|
+
// #806 — the remote-MCP broker: its Google OAuth client (a file) and seal
|
|
50
|
+
// key. Neither is a cms_admin credential.
|
|
51
|
+
"mcp-oauth-seal-key",
|
|
52
|
+
"mcp-oauth-client",
|
|
53
|
+
];
|
|
47
54
|
|
|
48
55
|
/** The source of a stack program. */
|
|
49
56
|
function stack(provider: "aws" | "azure" | "gcp" | "gcp-firebase"): string {
|
package/src/gcp-names.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function staticPublisherServiceAccountId(env: string): string {
|
|
|
69
69
|
export function gcpBucketName(
|
|
70
70
|
projectId: string,
|
|
71
71
|
env: string,
|
|
72
|
-
bucket: "media" | "static" | "staging",
|
|
72
|
+
bucket: "media" | "static" | "staging" | "audit-results",
|
|
73
73
|
): string {
|
|
74
74
|
return `${projectId}-${gcpNamePrefix(env)}-${bucket}`;
|
|
75
75
|
}
|
|
@@ -120,6 +120,48 @@ export function upgraderJobResource(projectId: string, region: string, env: stri
|
|
|
120
120
|
return `projects/${projectId}/locations/${region}/jobs/${upgraderJobName(env)}`;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Cloud Run job that installs releases which change the cloud setup (#819):
|
|
125
|
+
* a one-shot job from the signed caelo-ops image that runs as the install's
|
|
126
|
+
* `caelo-provisioner` account. The admin may only start it, without
|
|
127
|
+
* overrides (converge-job-setup.ts).
|
|
128
|
+
*/
|
|
129
|
+
export function convergeJobName(env: string): string {
|
|
130
|
+
return `${gcpNamePrefix(env)}-converge`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Full resource name of the converge job, as the admin's env carries it. */
|
|
134
|
+
export function convergeJobResource(projectId: string, region: string, env: string): string {
|
|
135
|
+
return `projects/${projectId}/locations/${region}/jobs/${convergeJobName(env)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* #875 — Cloud Run job that runs the quality audit's Lighthouse runs in
|
|
140
|
+
* parallel, one task per run (audit-job-setup.ts).
|
|
141
|
+
*/
|
|
142
|
+
export function auditJobName(env: string): string {
|
|
143
|
+
return `${gcpNamePrefix(env)}-audit`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Full resource name of the audit job, as the admin's env carries it. */
|
|
147
|
+
export function auditJobResource(projectId: string, region: string, env: string): string {
|
|
148
|
+
return `projects/${projectId}/locations/${region}/jobs/${auditJobName(env)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Account id of the audit job's own service account: reads the staged
|
|
153
|
+
* build, creates audit results, writes logs — nothing else
|
|
154
|
+
* (audit-job-setup.ts).
|
|
155
|
+
*/
|
|
156
|
+
export function auditServiceAccountId(env: string): string {
|
|
157
|
+
return `${gcpNamePrefix(env)}-audit`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Email of the audit job's service account. */
|
|
161
|
+
export function auditServiceAccountEmail(projectId: string, env: string): string {
|
|
162
|
+
return `${auditServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
/**
|
|
124
166
|
* Account id of the upgrader job's own service account: may roll the two
|
|
125
167
|
* Cloud Run services, run the migration job and back up Cloud SQL, never
|
package/src/gcp-url-map.test.ts
CHANGED
|
@@ -101,7 +101,15 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
101
101
|
},
|
|
102
102
|
);
|
|
103
103
|
expect(withRedirects.pathRules?.filter((r) => !isGcpRedirectRule(r))).toEqual([
|
|
104
|
-
{
|
|
104
|
+
{
|
|
105
|
+
paths: [
|
|
106
|
+
"/api/*",
|
|
107
|
+
// #806 — the remote-MCP broker's discovery documents.
|
|
108
|
+
"/.well-known/oauth-protected-resource/*",
|
|
109
|
+
"/.well-known/oauth-authorization-server/*",
|
|
110
|
+
],
|
|
111
|
+
service: "gateway-id",
|
|
112
|
+
},
|
|
105
113
|
]);
|
|
106
114
|
});
|
|
107
115
|
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,14 @@ export {
|
|
|
46
46
|
selfHostedCdnCopy,
|
|
47
47
|
} from "./cdn-copy.js";
|
|
48
48
|
export { type ComposeSpec, generateDockerCompose, type RolePasswords } from "./compose.js";
|
|
49
|
+
export {
|
|
50
|
+
type ConvergeClaim,
|
|
51
|
+
type ConvergeJobDeps,
|
|
52
|
+
type ConvergeQueue,
|
|
53
|
+
type ConvergeUpgradeOutcome,
|
|
54
|
+
runConvergeJob,
|
|
55
|
+
} from "./converge-job.js";
|
|
56
|
+
export { convergeExecutionNameError, gcpConvergeDeps } from "./converge-job-gcp.js";
|
|
49
57
|
export { INFRA_CONTRACT_VERSION } from "./infra-contract.js";
|
|
50
58
|
export {
|
|
51
59
|
INFRA_CONTRACT_PATHS,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Every infrastructure contract version names the step it adds, so the
|
|
5
|
-
* upgrader's refusal tells the Owner what the
|
|
5
|
+
* upgrader's refusal tells the Owner what the converge will change.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, it } from "bun:test";
|
|
@@ -20,13 +20,16 @@ describe("infrastructure contract steps", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("is at
|
|
24
|
-
expect(INFRA_CONTRACT_VERSION).toBe(
|
|
23
|
+
it("is at 10: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10)", () => {
|
|
24
|
+
expect(INFRA_CONTRACT_VERSION).toBe(10);
|
|
25
25
|
expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
|
|
26
26
|
expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
|
|
27
27
|
expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
|
|
28
28
|
expect(INFRA_CONTRACT_STEPS[6]).toContain("#799");
|
|
29
29
|
expect(INFRA_CONTRACT_STEPS[7]).toContain("#845");
|
|
30
|
+
expect(INFRA_CONTRACT_STEPS[8]).toContain("#875");
|
|
31
|
+
expect(INFRA_CONTRACT_STEPS[9]).toContain("#819");
|
|
32
|
+
expect(INFRA_CONTRACT_STEPS[10]).toContain("#806");
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
it("lists the pending steps oldest first", () => {
|
|
@@ -37,6 +40,6 @@ describe("infrastructure contract steps", () => {
|
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
it("names a version a newer release added by number", () => {
|
|
40
|
-
expect(infraStepsText(
|
|
43
|
+
expect(infraStepsText(10, 11)).toBe("infrastructure step 11 (see that release's notes)");
|
|
41
44
|
});
|
|
42
45
|
});
|