@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
package/src/tar-entries.ts
CHANGED
|
@@ -7,12 +7,34 @@
|
|
|
7
7
|
* otherwise land outside the target directory).
|
|
8
8
|
*
|
|
9
9
|
* Parses ustar headers plus the two extensions GNU tar and bsdtar emit for
|
|
10
|
-
* long names: GNU `L`/`K` records and pax `x
|
|
11
|
-
* `
|
|
10
|
+
* long names: GNU `L`/`K` records and pax `x` records (`path=`, `linkpath=`,
|
|
11
|
+
* `size=`). Anything it cannot read is an error, never a skipped member.
|
|
12
|
+
*
|
|
13
|
+
* The parser must walk the archive exactly as `tar -x` will: a pax `size=`
|
|
14
|
+
* overrides the header's size field for the next member, so ignoring it
|
|
15
|
+
* would let an archive hide a member from this check inside what the parser
|
|
16
|
+
* takes for a body. Records that rename a member in ways it does not model
|
|
17
|
+
* (pax global `g` headers, GNU sparse `GNU.sparse.*` keys) are refused.
|
|
18
|
+
*
|
|
19
|
+
* Wherever GNU tar and bsdtar read the same bytes differently, the archive is
|
|
20
|
+
* refused rather than read one way: no Caelo backup is ever written like that,
|
|
21
|
+
* and whichever reading this module picked, the other tar would extract
|
|
22
|
+
* something it never checked.
|
|
23
|
+
* - A header whose checksum does not match: GNU tar skips it and reads the
|
|
24
|
+
* next block as a header, so a member hidden in the "body" gets extracted.
|
|
25
|
+
* - A directory or symlink with a non-zero size: bsdtar reads no body for
|
|
26
|
+
* them, so the block after the header is the next member's header.
|
|
27
|
+
* - Two name records for one member (two pax `x` headers, two `L`/`K`, or
|
|
28
|
+
* `L`/`K` next to `x`): GNU tar keeps only the last `x` header, bsdtar
|
|
29
|
+
* flags the second as malformed and drops it, and the two tars disagree
|
|
30
|
+
* on whether a GNU long name or a pax path wins.
|
|
12
31
|
*/
|
|
13
32
|
|
|
14
33
|
const BLOCK = 512;
|
|
15
34
|
|
|
35
|
+
/** The extension headers one member may carry: one pax record, or a GNU long name and/or long link. */
|
|
36
|
+
const SINGLE_NAMING = new Set(["x", "L", "K", "LK", "KL"]);
|
|
37
|
+
|
|
16
38
|
export type TarEntryType = "file" | "directory" | "symlink" | "other";
|
|
17
39
|
|
|
18
40
|
export interface TarEntry {
|
|
@@ -30,15 +52,34 @@ export function listTarEntries(tar: Uint8Array): TarEntry[] {
|
|
|
30
52
|
let longLink: string | null = null;
|
|
31
53
|
let paxPath: string | null = null;
|
|
32
54
|
let paxLink: string | null = null;
|
|
55
|
+
let paxSize: number | null = null;
|
|
56
|
+
// The extension headers already seen for the member being read.
|
|
57
|
+
let extensions = "";
|
|
33
58
|
while (offset + BLOCK <= tar.length) {
|
|
34
59
|
const header = tar.subarray(offset, offset + BLOCK);
|
|
35
60
|
if (header.every((b) => b === 0)) break; // end-of-archive marker
|
|
36
|
-
|
|
61
|
+
verifyChecksum(header);
|
|
37
62
|
const typeflag = String.fromCharCode(header[156] ?? 0);
|
|
63
|
+
// A pax `size=` describes the member that follows the pax header, never
|
|
64
|
+
// another extension header (GNU tar applies it the same way).
|
|
65
|
+
const isExtension =
|
|
66
|
+
typeflag === "x" || typeflag === "g" || typeflag === "L" || typeflag === "K";
|
|
67
|
+
const size = !isExtension && paxSize !== null ? paxSize : octal(header.subarray(124, 136));
|
|
38
68
|
const body = tar.subarray(offset + BLOCK, offset + BLOCK + size);
|
|
39
69
|
if (body.length < size) throw new Error("truncated tar archive");
|
|
40
70
|
offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
|
|
41
71
|
|
|
72
|
+
if (typeflag === "g") {
|
|
73
|
+
throw new Error("tar archive contains unsupported pax global extended header");
|
|
74
|
+
}
|
|
75
|
+
if (isExtension) {
|
|
76
|
+
extensions += typeflag;
|
|
77
|
+
if (!SINGLE_NAMING.has(extensions)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`tar archive names one member through several extension headers (${extensions}) — tar implementations disagree on which one wins`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
42
83
|
if (typeflag === "L") {
|
|
43
84
|
longName = cString(body);
|
|
44
85
|
continue;
|
|
@@ -47,11 +88,18 @@ export function listTarEntries(tar: Uint8Array): TarEntry[] {
|
|
|
47
88
|
longLink = cString(body);
|
|
48
89
|
continue;
|
|
49
90
|
}
|
|
50
|
-
if (typeflag === "x"
|
|
91
|
+
if (typeflag === "x") {
|
|
51
92
|
const pax = paxRecords(body);
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
93
|
+
if (Object.keys(pax).some((key) => key.startsWith("GNU.sparse."))) {
|
|
94
|
+
throw new Error("tar archive contains an unsupported GNU sparse member");
|
|
95
|
+
}
|
|
96
|
+
paxPath = pax.path ?? null;
|
|
97
|
+
paxLink = pax.linkpath ?? null;
|
|
98
|
+
if (pax.size !== undefined) {
|
|
99
|
+
if (!/^[0-9]+$/.test(pax.size)) {
|
|
100
|
+
throw new Error(`unreadable pax size ${JSON.stringify(pax.size)}`);
|
|
101
|
+
}
|
|
102
|
+
paxSize = Number.parseInt(pax.size, 10);
|
|
55
103
|
}
|
|
56
104
|
continue;
|
|
57
105
|
}
|
|
@@ -60,6 +108,8 @@ export function listTarEntries(tar: Uint8Array): TarEntry[] {
|
|
|
60
108
|
const name = paxPath ?? longName ?? (prefix ? `${prefix}/${shortName}` : shortName);
|
|
61
109
|
const link = paxLink ?? longLink ?? cString(header.subarray(157, 257));
|
|
62
110
|
longName = longLink = paxPath = paxLink = null;
|
|
111
|
+
paxSize = null;
|
|
112
|
+
extensions = "";
|
|
63
113
|
const type: TarEntryType =
|
|
64
114
|
typeflag === "0" || typeflag === "\0" || typeflag === "7"
|
|
65
115
|
? "file"
|
|
@@ -68,6 +118,9 @@ export function listTarEntries(tar: Uint8Array): TarEntry[] {
|
|
|
68
118
|
: typeflag === "2"
|
|
69
119
|
? "symlink"
|
|
70
120
|
: "other";
|
|
121
|
+
if (type !== "file" && size !== 0) {
|
|
122
|
+
throw new Error(`tar member ${JSON.stringify(name)} is a ${type} with a body`);
|
|
123
|
+
}
|
|
71
124
|
entries.push(type === "symlink" ? { name, type, linkTarget: link } : { name, type });
|
|
72
125
|
}
|
|
73
126
|
return entries;
|
|
@@ -75,12 +128,40 @@ export function listTarEntries(tar: Uint8Array): TarEntry[] {
|
|
|
75
128
|
|
|
76
129
|
/** `./a/b/` → `a/b`; the archive root (`./`) → "". */
|
|
77
130
|
export function normalizeMemberName(name: string): string {
|
|
78
|
-
return name
|
|
131
|
+
return name
|
|
132
|
+
.replace(/\/+/g, "/")
|
|
133
|
+
.replace(/^(\.\/)+/, "")
|
|
134
|
+
.replace(/\/+$/, "");
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
/** Whether a member name could write outside the extraction directory. */
|
|
82
138
|
export function escapesRoot(name: string): boolean {
|
|
83
|
-
|
|
139
|
+
const normalized = name.replace(/\/+/g, "/").replace(/^(\.\/)+/, "");
|
|
140
|
+
return (
|
|
141
|
+
name.startsWith("/") || normalized.split("/").some((part) => part === ".." || part === ".")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Refuse a header whose checksum does not match, as `tar -x` would — except
|
|
147
|
+
* GNU tar then goes on reading at the next block, which this check must not.
|
|
148
|
+
* The sum counts the checksum field as spaces; old tars summed signed bytes,
|
|
149
|
+
* which GNU tar and bsdtar both still accept.
|
|
150
|
+
*/
|
|
151
|
+
function verifyChecksum(header: Uint8Array): void {
|
|
152
|
+
const recorded = cString(header.subarray(148, 156)).trim();
|
|
153
|
+
if (!/^[0-7]+$/.test(recorded)) throw new Error("unreadable tar header checksum");
|
|
154
|
+
let unsigned = 0;
|
|
155
|
+
let signed = 0;
|
|
156
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
157
|
+
const b = i >= 148 && i < 156 ? 0x20 : (header[i] ?? 0);
|
|
158
|
+
unsigned += b;
|
|
159
|
+
signed += b > 127 ? b - 256 : b;
|
|
160
|
+
}
|
|
161
|
+
const expected = Number.parseInt(recorded, 8);
|
|
162
|
+
if (expected !== unsigned && expected !== signed) {
|
|
163
|
+
throw new Error("tar header checksum mismatch — the archive is damaged or was altered");
|
|
164
|
+
}
|
|
84
165
|
}
|
|
85
166
|
|
|
86
167
|
function octal(field: Uint8Array): number {
|
package/src/upgrade/app-roll.ts
CHANGED
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
28
28
|
import { gatewayServiceAccountEmail, runServiceAccountEmail } from "../gcp-names.js";
|
|
29
29
|
import type { ImageDigests } from "../install-state.js";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
adminMediaVolume,
|
|
32
|
+
type CloudRunEnvVar,
|
|
33
|
+
GATEWAY_TIMEOUT_SECONDS,
|
|
34
|
+
SERVICE_SECRET_FILES,
|
|
35
|
+
} from "../stack-contract.js";
|
|
31
36
|
import {
|
|
32
37
|
type DeployedService,
|
|
33
38
|
type EnvChange,
|
|
@@ -35,13 +40,17 @@ import {
|
|
|
35
40
|
type LiveVolumes,
|
|
36
41
|
liveContainerEnv,
|
|
37
42
|
liveContainerMemory,
|
|
43
|
+
liveTimeoutSeconds,
|
|
38
44
|
liveVolumes,
|
|
39
45
|
planAdminMemory,
|
|
40
46
|
planContractEnv,
|
|
41
47
|
planMediaVolume,
|
|
48
|
+
planRequestTimeout,
|
|
49
|
+
planSecretFiles,
|
|
42
50
|
rollService,
|
|
43
51
|
serviceRollArgs,
|
|
44
52
|
} from "../stack-converge.js";
|
|
53
|
+
import { describeAuditJobImage, setAuditJobImage } from "./audit-job-roll.js";
|
|
45
54
|
import { restoreCommand, type SqlBackup } from "./backup.js";
|
|
46
55
|
import {
|
|
47
56
|
describeServiceJson,
|
|
@@ -88,15 +97,25 @@ export interface ServicePlan {
|
|
|
88
97
|
/** Container memory limit now (null = Cloud Run's default). */
|
|
89
98
|
readonly liveMemory: string | null;
|
|
90
99
|
readonly liveVolumes: LiveVolumes;
|
|
100
|
+
/** Request timeout now, in seconds (null = Cloud Run's default). */
|
|
101
|
+
readonly liveTimeout: number | null;
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
/** A service plan plus the changes its roll applies. */
|
|
94
105
|
export interface RollPlan extends ServicePlan {
|
|
95
106
|
readonly envFlags: readonly string[];
|
|
96
107
|
readonly envChanges: readonly EnvChange[];
|
|
97
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* #553 — e.g. `--memory=2Gi` when the admin runs below the stack default;
|
|
110
|
+
* #806 — `--timeout=3600` when the gateway runs below its contract timeout.
|
|
111
|
+
*/
|
|
98
112
|
readonly resourceFlags: readonly string[];
|
|
99
|
-
|
|
113
|
+
readonly resourceChanges: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* The admin's media bucket volume when it is (partly) missing, the
|
|
116
|
+
* service's secret files (#806, the gateway's OAuth client) when not
|
|
117
|
+
* mounted; empty otherwise.
|
|
118
|
+
*/
|
|
100
119
|
readonly volumeFlags: readonly string[];
|
|
101
120
|
readonly volumeChanges: readonly string[];
|
|
102
121
|
}
|
|
@@ -163,9 +182,10 @@ export async function planAppRoll(
|
|
|
163
182
|
liveEnv: liveContainerEnv(json),
|
|
164
183
|
liveMemory: liveContainerMemory(json),
|
|
165
184
|
liveVolumes: liveVolumes(json),
|
|
185
|
+
liveTimeout: liveTimeoutSeconds(json),
|
|
166
186
|
});
|
|
167
187
|
}
|
|
168
|
-
const env = planEnv(install, services,
|
|
188
|
+
const env = planEnv(install, services, {});
|
|
169
189
|
if (!env.ok)
|
|
170
190
|
return { ok: false, error: `the install's env contract can't be applied: ${env.error}` };
|
|
171
191
|
// Without the media volume every upload is lost on the next revision or
|
|
@@ -177,28 +197,60 @@ export async function planAppRoll(
|
|
|
177
197
|
);
|
|
178
198
|
if (!media.ok)
|
|
179
199
|
return { ok: false, error: `the admin's media volume can't be set up: ${media.error}` };
|
|
200
|
+
// #806 — each service's secret files (the gateway's remote-MCP OAuth
|
|
201
|
+
// client). A file the roll cannot mount fails the plan: the new gateway
|
|
202
|
+
// reads its path from the env contract.
|
|
203
|
+
const secretFiles = {} as Record<RollSlug, { flags: string[]; changes: string[] }>;
|
|
204
|
+
for (const s of services) {
|
|
205
|
+
const files = planSecretFiles(s.liveVolumes, SERVICE_SECRET_FILES[s.slug], install.env);
|
|
206
|
+
if (!files.ok) {
|
|
207
|
+
return { ok: false, error: `the ${s.slug}'s secret files can't be mounted: ${files.error}` };
|
|
208
|
+
}
|
|
209
|
+
secretFiles[s.slug] = files;
|
|
210
|
+
}
|
|
180
211
|
const warnings: string[] = [];
|
|
181
212
|
const rolls = services.map((s): RollPlan => {
|
|
182
213
|
// #553 — the admin runs the Lighthouse quality audit; raise it to the
|
|
183
214
|
// stack's memory default (never lowered; an unparsable value is kept).
|
|
184
215
|
const memory = s.slug === "admin" ? planAdminMemory(s.liveMemory) : null;
|
|
185
216
|
if (memory && !memory.ok) warnings.push(`admin memory left as is: ${memory.error}`);
|
|
217
|
+
// #806 — the gateway streams remote-MCP chat turns (stack-contract.ts
|
|
218
|
+
// GATEWAY_TIMEOUT_SECONDS). The admin's timeout is the stacks' alone.
|
|
219
|
+
const timeout =
|
|
220
|
+
s.slug === "gateway" ? planRequestTimeout(s.liveTimeout, GATEWAY_TIMEOUT_SECONDS) : null;
|
|
221
|
+
const memoryFlags = memory?.ok ? memory.flags : [];
|
|
222
|
+
const timeoutFlags = timeout?.ok ? timeout.flags : [];
|
|
186
223
|
return {
|
|
187
224
|
...s,
|
|
188
225
|
envFlags: env.services[s.slug].flags,
|
|
189
226
|
envChanges: env.services[s.slug].changes,
|
|
190
|
-
resourceFlags:
|
|
191
|
-
|
|
192
|
-
|
|
227
|
+
resourceFlags: [...memoryFlags, ...timeoutFlags],
|
|
228
|
+
resourceChanges: [
|
|
229
|
+
...memoryFlags.map((f) => `${s.liveMemory ?? "(default)"} → ${f}`),
|
|
230
|
+
...(timeout?.ok ? timeout.changes : []),
|
|
231
|
+
],
|
|
232
|
+
volumeFlags: [...(s.slug === "admin" ? media.flags : []), ...secretFiles[s.slug].flags],
|
|
233
|
+
volumeChanges: [...(s.slug === "admin" ? media.changes : []), ...secretFiles[s.slug].changes],
|
|
193
234
|
};
|
|
194
235
|
});
|
|
195
236
|
return { ok: true, plan: { install, services: rolls, warnings } };
|
|
196
237
|
}
|
|
197
238
|
|
|
239
|
+
/** Env the converge half adds to, or keeps out of, the rolls ({@link AppRollHooks.afterMigrations}). */
|
|
240
|
+
export interface EnvAdjustments {
|
|
241
|
+
/** Contract vars to leave exactly as the services have them. */
|
|
242
|
+
readonly leaveUntouched?: readonly string[];
|
|
243
|
+
/**
|
|
244
|
+
* Vars outside the env contract the rolls set too — #806's remote-MCP
|
|
245
|
+
* identity env, which only the converge (operator identity) can resolve.
|
|
246
|
+
*/
|
|
247
|
+
readonly extraEnv?: Readonly<Partial<Record<RollSlug, readonly CloudRunEnvVar[]>>>;
|
|
248
|
+
}
|
|
249
|
+
|
|
198
250
|
function planEnv(
|
|
199
251
|
install: AppRollInstall,
|
|
200
252
|
services: readonly ServicePlan[],
|
|
201
|
-
|
|
253
|
+
adjust: EnvAdjustments,
|
|
202
254
|
) {
|
|
203
255
|
const deployed = Object.fromEntries(
|
|
204
256
|
services.map((s) => [s.slug, { serviceName: s.serviceName, liveEnv: s.liveEnv }]),
|
|
@@ -213,17 +265,22 @@ function planEnv(
|
|
|
213
265
|
stagingHost: install.stagingHost,
|
|
214
266
|
},
|
|
215
267
|
deployed,
|
|
216
|
-
{
|
|
268
|
+
{
|
|
269
|
+
leaveUntouched: adjust.leaveUntouched ?? [],
|
|
270
|
+
...(adjust.extraEnv ? { extraEnv: adjust.extraEnv } : {}),
|
|
271
|
+
},
|
|
217
272
|
);
|
|
218
273
|
}
|
|
219
274
|
|
|
220
275
|
/**
|
|
221
|
-
* The plan with
|
|
222
|
-
*
|
|
276
|
+
* The plan with the converge's adjustments: vars left exactly as the
|
|
277
|
+
* services have them (they point at something a converge step could not set
|
|
278
|
+
* up) and vars outside the contract the rolls set too.
|
|
223
279
|
*/
|
|
224
|
-
export function
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
export function adjustingEnv(plan: AppRollPlan, adjust: EnvAdjustments): AppRollPlan {
|
|
281
|
+
const extra = Object.values(adjust.extraEnv ?? {}).some((v) => (v?.length ?? 0) > 0);
|
|
282
|
+
if ((adjust.leaveUntouched?.length ?? 0) === 0 && !extra) return plan;
|
|
283
|
+
const env = planEnv(plan.install, plan.services, adjust);
|
|
227
284
|
if (!env.ok) return plan;
|
|
228
285
|
return {
|
|
229
286
|
...plan,
|
|
@@ -239,10 +296,8 @@ export function leavingEnvUntouched(plan: AppRollPlan, names: readonly string[])
|
|
|
239
296
|
export function reportPlan(plan: AppRollPlan, reporter: UpgradeReporter): void {
|
|
240
297
|
for (const w of plan.warnings) reporter.warn(w);
|
|
241
298
|
for (const s of plan.services) {
|
|
242
|
-
for (const c of s.volumeChanges) reporter.info(`${s.slug}
|
|
243
|
-
for (const
|
|
244
|
-
reporter.info(`${s.slug} resources: ${s.liveMemory ?? "(default)"} → ${f}`);
|
|
245
|
-
}
|
|
299
|
+
for (const c of s.volumeChanges) reporter.info(`${s.slug} volumes: ${c}`);
|
|
300
|
+
for (const c of s.resourceChanges) reporter.info(`${s.slug} resources: ${c}`);
|
|
246
301
|
for (const c of s.envChanges) {
|
|
247
302
|
reporter.info(
|
|
248
303
|
`${s.slug} env ${c.name}: ${c.from === undefined ? "(unset)" : c.from} → ${c.to === undefined ? "(removed)" : c.to}`,
|
|
@@ -259,7 +314,11 @@ export interface AppRollHooks {
|
|
|
259
314
|
* not be set up; `ok: false` aborts the upgrade (nothing rolled yet).
|
|
260
315
|
*/
|
|
261
316
|
afterMigrations?(): Promise<
|
|
262
|
-
{ readonly ok: true; readonly leaveUntouched: readonly string[] }
|
|
317
|
+
| ({ readonly ok: true; readonly leaveUntouched: readonly string[] } & Pick<
|
|
318
|
+
EnvAdjustments,
|
|
319
|
+
"extraEnv"
|
|
320
|
+
>)
|
|
321
|
+
| { readonly ok: false }
|
|
263
322
|
>;
|
|
264
323
|
/** Right before `slug` rolls; `ok: false` aborts and rolls back what rolled. */
|
|
265
324
|
beforeRoll?(
|
|
@@ -308,6 +367,13 @@ export type AppRollResult =
|
|
|
308
367
|
readonly phase: AppRollPhase;
|
|
309
368
|
/** Whether migrations ran before the failure (the backup matters then). */
|
|
310
369
|
readonly migrated: boolean;
|
|
370
|
+
/**
|
|
371
|
+
* Whether services had rolled and EVERY one of them was pinned back to
|
|
372
|
+
* the revision that served before — i.e. the previous release serves
|
|
373
|
+
* again. False when nothing had rolled, or when a pin failed (then a
|
|
374
|
+
* person must pin it; the reporter named which).
|
|
375
|
+
*/
|
|
376
|
+
readonly rolledBack: boolean;
|
|
311
377
|
};
|
|
312
378
|
|
|
313
379
|
/** Run the app-roll. See the file comment for the sequence. */
|
|
@@ -319,10 +385,13 @@ export async function executeAppRoll(
|
|
|
319
385
|
const { reporter: r, run } = deps;
|
|
320
386
|
const { install } = input.plan;
|
|
321
387
|
let plan = input.plan;
|
|
388
|
+
/** Set by {@link rollBack}: null until one ran with services to restore. */
|
|
389
|
+
let rollbackOk: boolean | null = null;
|
|
322
390
|
const fail = (phase: AppRollPhase, migrated: boolean): AppRollResult => ({
|
|
323
391
|
ok: false,
|
|
324
392
|
phase,
|
|
325
393
|
migrated,
|
|
394
|
+
rolledBack: rollbackOk === true,
|
|
326
395
|
});
|
|
327
396
|
|
|
328
397
|
// A probe that cannot reach the install now would only fail after the
|
|
@@ -362,14 +431,28 @@ export async function executeAppRoll(
|
|
|
362
431
|
if (hooks.afterMigrations) {
|
|
363
432
|
const converged = await hooks.afterMigrations();
|
|
364
433
|
if (!converged.ok) return fail("converge", true);
|
|
365
|
-
plan =
|
|
434
|
+
plan = adjustingEnv(plan, {
|
|
435
|
+
leaveUntouched: converged.leaveUntouched,
|
|
436
|
+
...(converged.extraEnv ? { extraEnv: converged.extraEnv } : {}),
|
|
437
|
+
});
|
|
366
438
|
}
|
|
367
439
|
|
|
368
440
|
const rolled: RollPlan[] = [];
|
|
441
|
+
/** The audit job's image before this roll moved it (#875); null = not moved. */
|
|
442
|
+
let auditJobPriorImage: string | null = null;
|
|
369
443
|
const rollBack = async (alsoCurrent?: RollPlan): Promise<void> => {
|
|
444
|
+
if (auditJobPriorImage) {
|
|
445
|
+
const back = await setAuditJobImage(run, install, auditJobPriorImage);
|
|
446
|
+
if (back.ok) r.info(` audit job image restored to ${auditJobPriorImage.slice(-19)}`);
|
|
447
|
+
else
|
|
448
|
+
r.error(
|
|
449
|
+
` audit job rollback FAILED (${back.error}) — set its image to ${auditJobPriorImage} by hand`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
370
452
|
const services = alsoCurrent ? [...rolled, alsoCurrent] : rolled;
|
|
371
453
|
if (services.length === 0) return;
|
|
372
454
|
r.warn(`Rolling back ${services.length} service(s) to the revisions that served before...`);
|
|
455
|
+
rollbackOk = true;
|
|
373
456
|
for (const p of [...services].reverse()) {
|
|
374
457
|
const ok = await pinTraffic(
|
|
375
458
|
run,
|
|
@@ -379,7 +462,10 @@ export async function executeAppRoll(
|
|
|
379
462
|
p.priorRevision,
|
|
380
463
|
);
|
|
381
464
|
if (ok) r.info(` ${p.slug} traffic restored to ${p.priorRevision}`);
|
|
382
|
-
else
|
|
465
|
+
else {
|
|
466
|
+
rollbackOk = false;
|
|
467
|
+
r.error(` ${p.slug} rollback FAILED — pin ${p.priorRevision} by hand`);
|
|
468
|
+
}
|
|
383
469
|
await hooks.afterRollback?.(p.slug);
|
|
384
470
|
}
|
|
385
471
|
};
|
|
@@ -447,6 +533,34 @@ export async function executeAppRoll(
|
|
|
447
533
|
rolled.push(p);
|
|
448
534
|
}
|
|
449
535
|
|
|
536
|
+
// #875 — the audit job runs the admin's image; it follows the admin. A
|
|
537
|
+
// failure here does not undo the release: audits then fail loudly with
|
|
538
|
+
// the version mismatch and the fix, until the next upgrade moves it.
|
|
539
|
+
const sAudit = r.step("Moving the quality-audit job onto the admin's image...");
|
|
540
|
+
const described = await describeAuditJobImage(run, install);
|
|
541
|
+
const auditPrior = described.ok ? described.image : null;
|
|
542
|
+
if (!described.ok) {
|
|
543
|
+
sAudit.warned(
|
|
544
|
+
`Could not read the quality-audit job (${described.error}); Stage audits fail until the next upgrade moves it.`,
|
|
545
|
+
);
|
|
546
|
+
} else if (!auditPrior) {
|
|
547
|
+
sAudit.warned(
|
|
548
|
+
"The install has no quality-audit job yet; Stage audits fail until `cms-provision upgrade` sets it up.",
|
|
549
|
+
);
|
|
550
|
+
} else if (auditPrior === admin.imageRef) {
|
|
551
|
+
sAudit.done("Quality-audit job already on the admin's image");
|
|
552
|
+
} else {
|
|
553
|
+
const moved = await setAuditJobImage(run, install, admin.imageRef);
|
|
554
|
+
if (moved.ok) {
|
|
555
|
+
auditJobPriorImage = auditPrior;
|
|
556
|
+
sAudit.done(`Quality-audit job on ${admin.digest.slice(0, 19)}...`);
|
|
557
|
+
} else {
|
|
558
|
+
sAudit.warned(
|
|
559
|
+
`Quality-audit job not moved (${moved.error}); Stage audits fail until the next upgrade moves it.`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
450
564
|
if (deps.health) {
|
|
451
565
|
const s = r.step("Deep health check (database, migrations, version)...");
|
|
452
566
|
// A probe that throws is a failed check: roll back, never leave the
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #875 — the quality-audit job runs the admin image (admin-core
|
|
5
|
+
* quality/audit-task.ts) and refuses a batch from an admin on another
|
|
6
|
+
* version. So every app-roll moves the job onto the admin's new image right
|
|
7
|
+
* after the services rolled, and a rollback moves it back.
|
|
8
|
+
*
|
|
9
|
+
* `run jobs update --image` changes nothing else of the job (identity, env,
|
|
10
|
+
* parallelism), so this stays inside the app-roll's rights: the upgrader's
|
|
11
|
+
* `roles/run.developer` plus actAs on the job's service account
|
|
12
|
+
* (upgrader-setup.ts).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { GcloudRunner } from "../gcloud-retry.js";
|
|
16
|
+
import { auditJobName } from "../gcp-names.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The job's image now; `image: null` only when the install has no audit job
|
|
20
|
+
* yet. Any other gcloud failure (permissions, API, network) is an error, not
|
|
21
|
+
* "no job", so the app-roll reports it instead of a misleading hint.
|
|
22
|
+
*/
|
|
23
|
+
export async function describeAuditJobImage(
|
|
24
|
+
run: GcloudRunner,
|
|
25
|
+
install: { readonly projectId: string; readonly region: string; readonly env: string },
|
|
26
|
+
): Promise<
|
|
27
|
+
| { readonly ok: true; readonly image: string | null }
|
|
28
|
+
| { readonly ok: false; readonly error: string }
|
|
29
|
+
> {
|
|
30
|
+
const r = await run([
|
|
31
|
+
"run",
|
|
32
|
+
"jobs",
|
|
33
|
+
"describe",
|
|
34
|
+
auditJobName(install.env),
|
|
35
|
+
`--region=${install.region}`,
|
|
36
|
+
`--project=${install.projectId}`,
|
|
37
|
+
"--format=value(spec.template.spec.template.spec.containers[0].image)",
|
|
38
|
+
]);
|
|
39
|
+
if (!r.ok) {
|
|
40
|
+
return /NOT_FOUND|not found|could not be found/i.test(r.stderr)
|
|
41
|
+
? { ok: true, image: null }
|
|
42
|
+
: { ok: false, error: r.stderr.trim() };
|
|
43
|
+
}
|
|
44
|
+
const image = r.stdout.trim();
|
|
45
|
+
if (image.length === 0) {
|
|
46
|
+
return { ok: false, error: "the job's description names no container image" };
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, image };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Point the audit job at `imageRef`. */
|
|
52
|
+
export async function setAuditJobImage(
|
|
53
|
+
run: GcloudRunner,
|
|
54
|
+
install: { readonly projectId: string; readonly region: string; readonly env: string },
|
|
55
|
+
imageRef: string,
|
|
56
|
+
): Promise<{ readonly ok: boolean; readonly error?: string }> {
|
|
57
|
+
const r = await run([
|
|
58
|
+
"run",
|
|
59
|
+
"jobs",
|
|
60
|
+
"update",
|
|
61
|
+
auditJobName(install.env),
|
|
62
|
+
`--image=${imageRef}`,
|
|
63
|
+
`--region=${install.region}`,
|
|
64
|
+
`--project=${install.projectId}`,
|
|
65
|
+
"--quiet",
|
|
66
|
+
]);
|
|
67
|
+
return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim() };
|
|
68
|
+
}
|