@caelo-cms/provisioning 0.11.8 → 0.11.10
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/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +6 -1
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +86 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +20 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +65 -3
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +12 -3
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +63 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +289 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +29 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +95 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +2 -11
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +21 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +24 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +24 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-state.d.ts +6 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +17 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- package/dist/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +9 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +21 -10
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +34 -0
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +52 -0
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +15 -6
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +72 -23
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +170 -25
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +314 -37
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +99 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +13 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +34 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +49 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +22 -7
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +43 -17
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +1 -0
- package/src/adopt-existing.test.ts +39 -0
- package/src/adopt-existing.ts +83 -1
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +36 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +3 -0
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli.ts +12 -1
- package/src/cloudsql-tls.test.ts +225 -0
- package/src/cloudsql-tls.ts +352 -0
- package/src/compose.ts +125 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-job-gcp.ts +2 -11
- package/src/database-tls-builders.test.ts +55 -0
- package/src/deployed-release.test.ts +104 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +10 -5
- package/src/gcp-names.ts +31 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +7 -1
- package/src/index.ts +1 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +24 -1
- package/src/install-state.ts +26 -4
- package/src/lifecycle.ts +20 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +5 -4
- package/src/pool-budget-stacks.test.ts +1 -1
- package/src/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +20 -7
- package/src/runtime-secrets.ts +50 -9
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-compose.test.ts +108 -5
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.test.ts +54 -0
- package/src/self-hosted-roles.ts +58 -0
- package/src/self-hosted-start.test.ts +34 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +103 -14
- package/src/self-hosted-upgrade.ts +115 -29
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +169 -29
- package/src/stack-contract.ts +362 -42
- package/src/stack-converge-secrets.test.ts +26 -13
- package/src/stack-converge.test.ts +199 -26
- package/src/stack-converge.ts +92 -30
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +120 -11
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +353 -44
- package/src/upgrade/upgrade.ts +55 -6
- package/src/upgrader-gcp.ts +65 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizards/gcp-cost.test.ts +50 -0
- package/src/wizards/gcp-cost.ts +24 -7
- package/src/wizards/gcp.ts +55 -14
- package/stacks/aws/index.ts +32 -14
- package/stacks/azure/index.ts +57 -18
- package/stacks/gcp/index.ts +83 -23
- package/stacks/gcp-firebase/index.ts +162 -33
- package/stacks/self-hosted/README.md +2 -1
- package/stacks/self-hosted/index.ts +11 -1
package/src/stack-converge.ts
CHANGED
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
* Storage volume + mount), added in the same `services update` when
|
|
24
24
|
* missing.
|
|
25
25
|
* - {@link planSecretFiles} / {@link planRequestTimeout} (#806): the
|
|
26
|
-
*
|
|
26
|
+
* MCP-Auth-Proxy.s secret file (the remote-MCP OAuth client) and its request
|
|
27
27
|
* timeout, in the same `services update`.
|
|
28
28
|
* - {@link planCpuIdle}: request-based CPU where the contract wants it
|
|
29
29
|
* (`SERVICE_CPU_IDLE`), in the same `services update`.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import { isIpLiteral } from "@caelo-cms/shared";
|
|
32
33
|
import { removeDefaultFirewallRules } from "./default-firewall.js";
|
|
33
34
|
import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
|
|
34
35
|
import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
|
|
@@ -38,8 +39,10 @@ import {
|
|
|
38
39
|
ADMIN_MEMORY_DEFAULT,
|
|
39
40
|
type AdminEnvInputs,
|
|
40
41
|
adminEnvContract,
|
|
42
|
+
CLOUD_RUN_SLUGS,
|
|
41
43
|
type CloudRunEnvVar,
|
|
42
44
|
type CloudRunSlug,
|
|
45
|
+
DATABASE_URL_VARS,
|
|
43
46
|
databaseUrls,
|
|
44
47
|
type GcpProvider,
|
|
45
48
|
gatewayEnvContract,
|
|
@@ -47,6 +50,7 @@ import {
|
|
|
47
50
|
type IamTarget,
|
|
48
51
|
iamMember,
|
|
49
52
|
type MediaVolumeContract,
|
|
53
|
+
mcpAuthProxyEnvContract,
|
|
50
54
|
memoryQuantityMiB,
|
|
51
55
|
RETIRED_IAM_BINDINGS,
|
|
52
56
|
RETIRED_RUNTIME_SECRETS,
|
|
@@ -167,7 +171,7 @@ function maskUrlPassword(value: string): string {
|
|
|
167
171
|
* gcloud's list flags split on `,`; a value containing one needs the
|
|
168
172
|
* `^<delim>^` escape syntax (`gcloud topic escaping`).
|
|
169
173
|
*/
|
|
170
|
-
function listFlag(flag: string, pairs: string[]): string {
|
|
174
|
+
export function listFlag(flag: string, pairs: string[]): string {
|
|
171
175
|
const joined = pairs.join(",");
|
|
172
176
|
if (!pairs.some((p) => p.includes(","))) return `${flag}=${joined}`;
|
|
173
177
|
const delim = ["@", "|", "#", "~", ";"].find((d) => !pairs.some((p) => p.includes(d)));
|
|
@@ -306,21 +310,42 @@ export function planContractEnv(
|
|
|
306
310
|
* refused rather than silently doubled.
|
|
307
311
|
*/
|
|
308
312
|
readonly extraEnv?: Readonly<Partial<Record<CloudRunSlug, readonly CloudRunEnvVar[]>>>;
|
|
313
|
+
/**
|
|
314
|
+
* #1338 — the database host the converge just set up (the instance's
|
|
315
|
+
* DNS name, cloudsql-tls.ts), in place of the one the services dialled
|
|
316
|
+
* when the plan was read.
|
|
317
|
+
*/
|
|
318
|
+
readonly databaseHost?: string;
|
|
309
319
|
} = {},
|
|
310
320
|
):
|
|
311
321
|
| {
|
|
312
322
|
readonly ok: true;
|
|
313
323
|
readonly services: Record<CloudRunSlug, { flags: string[]; changes: EnvChange[] }>;
|
|
324
|
+
/**
|
|
325
|
+
* #1338 — the services still dial the instance's IP, which no
|
|
326
|
+
* verify-full connection can use; their database URLs were left as
|
|
327
|
+
* they are. Only a converge moves them (cloudsql-tls.ts); a roll must
|
|
328
|
+
* not start while this is true.
|
|
329
|
+
*/
|
|
330
|
+
readonly databaseHostPending: boolean;
|
|
314
331
|
}
|
|
315
332
|
| { readonly ok: false; readonly error: string } {
|
|
316
|
-
const
|
|
317
|
-
if (!
|
|
333
|
+
const live = liveDatabaseHost(services.admin.liveEnv);
|
|
334
|
+
if (!opts.databaseHost && !live.ok) {
|
|
318
335
|
return {
|
|
319
336
|
ok: false,
|
|
320
|
-
error: `the Cloud SQL host is unknown: the admin service (${services.admin.serviceName}) ${
|
|
337
|
+
error: `the Cloud SQL host is unknown: the admin service (${services.admin.serviceName}) ${live.error}`,
|
|
321
338
|
};
|
|
322
339
|
}
|
|
323
|
-
const
|
|
340
|
+
const host = opts.databaseHost ?? (live.ok ? live.host : "");
|
|
341
|
+
const databaseHostPending = isIpLiteral(host);
|
|
342
|
+
// An IP cannot carry a verify-full URL (databaseUrls refuses it): the URLs
|
|
343
|
+
// are planned from a placeholder name and then left untouched, so the
|
|
344
|
+
// rest of the env still plans and the caller can see what is pending.
|
|
345
|
+
const runtime: RuntimeEnvInputs = {
|
|
346
|
+
...install,
|
|
347
|
+
databaseUrls: databaseUrls(databaseHostPending ? "pending.invalid" : host),
|
|
348
|
+
};
|
|
324
349
|
let inputs: AdminEnvInputs;
|
|
325
350
|
if (install.provider === "gcp-firebase") {
|
|
326
351
|
// The site id carries a random suffix Pulumi generated at install time.
|
|
@@ -340,36 +365,33 @@ export function planContractEnv(
|
|
|
340
365
|
} else {
|
|
341
366
|
inputs = { ...runtime, provider: "gcp" };
|
|
342
367
|
}
|
|
343
|
-
const skip = new Set(
|
|
368
|
+
const skip = new Set([
|
|
369
|
+
...(opts.leaveUntouched ?? []),
|
|
370
|
+
...(databaseHostPending ? [...DATABASE_URL_VARS.admin, ...DATABASE_URL_VARS.gateway] : []),
|
|
371
|
+
]);
|
|
344
372
|
const desired = (slug: CloudRunSlug, contract: readonly CloudRunEnvVar[]) => {
|
|
345
373
|
const extra = opts.extraEnv?.[slug] ?? [];
|
|
346
374
|
const clash = extra.find((e) => contract.some((c) => c.name === e.name));
|
|
347
375
|
if (clash) throw new Error(`${slug}: ${clash.name} is both in the contract and extra env`);
|
|
348
376
|
return [...contract, ...extra].filter((e) => !skip.has(e.name));
|
|
349
377
|
};
|
|
350
|
-
let
|
|
351
|
-
let gatewayDesired: CloudRunEnvVar[];
|
|
378
|
+
let contracts: Record<CloudRunSlug, CloudRunEnvVar[]>;
|
|
352
379
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
380
|
+
contracts = {
|
|
381
|
+
admin: desired("admin", adminEnvContract(inputs)),
|
|
382
|
+
gateway: desired("gateway", gatewayEnvContract(runtime)),
|
|
383
|
+
"mcp-auth-proxy": desired("mcp-auth-proxy", mcpAuthProxyEnvContract(install)),
|
|
384
|
+
};
|
|
355
385
|
} catch (e) {
|
|
356
386
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
357
387
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!gateway.ok) return { ok: false, error: `gateway: ${gateway.error}` };
|
|
366
|
-
return {
|
|
367
|
-
ok: true,
|
|
368
|
-
services: {
|
|
369
|
-
admin: { flags: admin.flags, changes: admin.changes },
|
|
370
|
-
gateway: { flags: gateway.flags, changes: gateway.changes },
|
|
371
|
-
},
|
|
372
|
-
};
|
|
388
|
+
const out = {} as Record<CloudRunSlug, { flags: string[]; changes: EnvChange[] }>;
|
|
389
|
+
for (const slug of CLOUD_RUN_SLUGS) {
|
|
390
|
+
const plan = planEnvUpdate(services[slug].liveEnv, contracts[slug], RETIRED_SERVICE_ENV[slug]);
|
|
391
|
+
if (!plan.ok) return { ok: false, error: `${slug}: ${plan.error}` };
|
|
392
|
+
out[slug] = { flags: plan.flags, changes: plan.changes };
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, services: out, databaseHostPending };
|
|
373
395
|
}
|
|
374
396
|
|
|
375
397
|
// ===========================================================================
|
|
@@ -618,16 +640,24 @@ export function liveIngress(serviceJson: string): string | null {
|
|
|
618
640
|
* the serving one and the app-roll's rollback target stay as they are. On
|
|
619
641
|
* `gcp` the stack declares load-balancer-only and `staging-domain enable`
|
|
620
642
|
* (admin-domain.ts) opens it deliberately, so this never runs there.
|
|
643
|
+
*
|
|
644
|
+
* #1328 — both public services of a gcp-firebase install: the data gateway
|
|
645
|
+
* (the Firebase rewrite, the health probe) and the MCP-Auth-Proxy (remote-MCP
|
|
646
|
+
* clients, and until #1331 the staging mapping it forwards).
|
|
621
647
|
*/
|
|
622
|
-
async function
|
|
648
|
+
async function ensurePublicIngress(
|
|
623
649
|
install: InstallTarget,
|
|
650
|
+
slug: "gateway" | "mcp-auth-proxy",
|
|
624
651
|
run: GcloudRunner,
|
|
625
652
|
): Promise<InvariantOutcome> {
|
|
626
|
-
const service = install.services
|
|
653
|
+
const service = install.services[slug];
|
|
627
654
|
const base = {
|
|
628
655
|
id: `ingress all on ${service}`,
|
|
629
656
|
onFailure: "abort" as const,
|
|
630
|
-
why:
|
|
657
|
+
why:
|
|
658
|
+
slug === "gateway"
|
|
659
|
+
? "the gcp-firebase stack's gateway ingress (#650); Firebase Hosting and the health check reach its run.app URL"
|
|
660
|
+
: "the gcp-firebase stack's MCP-Auth-Proxy ingress (#1328); remote-MCP clients, the health check and the staging domain reach its run.app URL",
|
|
631
661
|
};
|
|
632
662
|
const where = [`--region=${install.region}`, `--project=${install.projectId}`];
|
|
633
663
|
const describe = await run(["run", "services", "describe", service, ...where, "--format=json"]);
|
|
@@ -697,7 +727,10 @@ export async function ensureStackInvariants(
|
|
|
697
727
|
outcomes.push(await ensureCdnPolicy(install, run));
|
|
698
728
|
outcomes.push(await ensureSvgDownloads(install, run));
|
|
699
729
|
}
|
|
700
|
-
if (install.provider === "gcp-firebase")
|
|
730
|
+
if (install.provider === "gcp-firebase") {
|
|
731
|
+
outcomes.push(await ensurePublicIngress(install, "gateway", run));
|
|
732
|
+
outcomes.push(await ensurePublicIngress(install, "mcp-auth-proxy", run));
|
|
733
|
+
}
|
|
701
734
|
outcomes.push(...(await removeDefaultFirewallRules(install.projectId, { run })));
|
|
702
735
|
|
|
703
736
|
return {
|
|
@@ -1183,6 +1216,35 @@ export function planCpuIdle(live: boolean, desired: boolean): RollAdditionsPlan
|
|
|
1183
1216
|
};
|
|
1184
1217
|
}
|
|
1185
1218
|
|
|
1219
|
+
/**
|
|
1220
|
+
* Whether the service's revision template egresses into a VPC (Direct VPC
|
|
1221
|
+
* egress, the stacks' `vpcAccess.networkInterfaces`).
|
|
1222
|
+
*/
|
|
1223
|
+
export function liveDirectVpc(serviceJson: string): boolean {
|
|
1224
|
+
const svc = JSON.parse(serviceJson) as {
|
|
1225
|
+
spec?: { template?: { metadata?: { annotations?: Record<string, string> } } };
|
|
1226
|
+
};
|
|
1227
|
+
return Boolean(
|
|
1228
|
+
svc.spec?.template?.metadata?.annotations?.["run.googleapis.com/network-interfaces"],
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* #1328 — `--clear-network` when a service the contract keeps off the VPC
|
|
1234
|
+
* still egresses into it. The MCP-Auth-Proxy on gcp-firebase is the service
|
|
1235
|
+
* that was the gateway, which reached Cloud SQL's private IP that way; the
|
|
1236
|
+
* proxy talks to the admin's public URL only, so it loses the route to the
|
|
1237
|
+
* database's network together with its database credentials.
|
|
1238
|
+
*/
|
|
1239
|
+
export function planNoVpc(live: boolean): RollAdditionsPlan {
|
|
1240
|
+
if (!live) return { ok: true, flags: [], changes: [] };
|
|
1241
|
+
return {
|
|
1242
|
+
ok: true,
|
|
1243
|
+
flags: ["--clear-network"],
|
|
1244
|
+
changes: ["VPC egress (private ranges) → none"],
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1186
1248
|
/**
|
|
1187
1249
|
* The single `gcloud run services update` that rolls a service to a new
|
|
1188
1250
|
* image, its run SA and its env changes, so all land in one new revision.
|
package/src/state-store.test.ts
CHANGED
|
@@ -36,7 +36,8 @@ function fakeBucket() {
|
|
|
36
36
|
return { objects, calls, run };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const PASSWORD_URL =
|
|
39
|
+
const PASSWORD_URL =
|
|
40
|
+
"postgres://admin_role:hunter2hunter2@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin";
|
|
40
41
|
|
|
41
42
|
let root = "";
|
|
42
43
|
beforeEach(() => {
|
package/src/upgrade/app-roll.ts
CHANGED
|
@@ -25,15 +25,23 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
gatewayServiceAccountEmail,
|
|
30
|
+
mcpAuthProxyServiceAccountEmail,
|
|
31
|
+
runServiceAccountEmail,
|
|
32
|
+
} from "../gcp-names.js";
|
|
29
33
|
import type { ImageDigests } from "../install-state.js";
|
|
30
34
|
import {
|
|
31
35
|
adminMediaVolume,
|
|
36
|
+
CLOUD_RUN_SLUGS,
|
|
32
37
|
type CloudRunEnvVar,
|
|
33
|
-
|
|
38
|
+
type CloudRunSlug,
|
|
39
|
+
cloudRunServicePrefix,
|
|
40
|
+
MCP_AUTH_PROXY_TIMEOUT_SECONDS,
|
|
34
41
|
RETIRED_SECRET_FILES,
|
|
35
42
|
SERVICE_CPU_IDLE,
|
|
36
43
|
SERVICE_SECRET_FILES,
|
|
44
|
+
SERVICE_VPC_EGRESS,
|
|
37
45
|
} from "../stack-contract.js";
|
|
38
46
|
import {
|
|
39
47
|
type DeployedService,
|
|
@@ -44,12 +52,14 @@ import {
|
|
|
44
52
|
liveContainerEnv,
|
|
45
53
|
liveContainerMemory,
|
|
46
54
|
liveCpuIdle,
|
|
55
|
+
liveDirectVpc,
|
|
47
56
|
liveTimeoutSeconds,
|
|
48
57
|
liveVolumes,
|
|
49
58
|
planAdminMemory,
|
|
50
59
|
planContractEnv,
|
|
51
60
|
planCpuIdle,
|
|
52
61
|
planMediaVolume,
|
|
62
|
+
planNoVpc,
|
|
53
63
|
planRequestTimeout,
|
|
54
64
|
planSecretFiles,
|
|
55
65
|
rollService,
|
|
@@ -69,10 +79,27 @@ import {
|
|
|
69
79
|
import type { HealthProbe } from "./health.js";
|
|
70
80
|
import type { UpgradeReporter } from "./reporter.js";
|
|
71
81
|
|
|
72
|
-
export type RollSlug =
|
|
82
|
+
export type RollSlug = CloudRunSlug;
|
|
73
83
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The roll order: the gateway follows the admin, never the other way round,
|
|
86
|
+
* and the MCP-Auth-Proxy comes last (#1328) — on gcp-firebase it forwards
|
|
87
|
+
* the visitor traffic still addressed to it to the gateway, which must
|
|
88
|
+
* already serve the new release by then.
|
|
89
|
+
*/
|
|
90
|
+
export const ROLL_ORDER: readonly RollSlug[] = CLOUD_RUN_SLUGS;
|
|
91
|
+
|
|
92
|
+
/** The service account each service's new revision runs as. */
|
|
93
|
+
export function rollServiceAccount(slug: RollSlug, projectId: string, env: string): string {
|
|
94
|
+
switch (slug) {
|
|
95
|
+
case "admin":
|
|
96
|
+
return runServiceAccountEmail(projectId, env);
|
|
97
|
+
case "gateway":
|
|
98
|
+
return gatewayServiceAccountEmail(projectId, env);
|
|
99
|
+
case "mcp-auth-proxy":
|
|
100
|
+
return mcpAuthProxyServiceAccountEmail(projectId, env);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
76
103
|
|
|
77
104
|
/** The install an app-roll acts on. */
|
|
78
105
|
export interface AppRollInstall {
|
|
@@ -110,6 +137,8 @@ export interface ServicePlan {
|
|
|
110
137
|
readonly liveTimeout: number | null;
|
|
111
138
|
/** Whether the service gets CPU only during requests now. */
|
|
112
139
|
readonly liveCpuIdle: boolean;
|
|
140
|
+
/** Whether the service egresses into the install's VPC now. */
|
|
141
|
+
readonly liveDirectVpc: boolean;
|
|
113
142
|
}
|
|
114
143
|
|
|
115
144
|
/** A service plan plus the changes its roll applies. */
|
|
@@ -139,6 +168,13 @@ export interface AppRollPlan {
|
|
|
139
168
|
readonly services: readonly RollPlan[];
|
|
140
169
|
/** Notes for the operator (e.g. a memory limit left as is). */
|
|
141
170
|
readonly warnings: readonly string[];
|
|
171
|
+
/**
|
|
172
|
+
* #1338 — the services dial the Cloud SQL instance's IP, which no
|
|
173
|
+
* verify-full connection can use, so their database URLs are not planned.
|
|
174
|
+
* A converge moves them onto the instance's DNS name
|
|
175
|
+
* ({@link EnvAdjustments.databaseHost}); a roll refuses while this holds.
|
|
176
|
+
*/
|
|
177
|
+
readonly databaseHostPending?: boolean;
|
|
142
178
|
}
|
|
143
179
|
|
|
144
180
|
export type PlanResult =
|
|
@@ -166,7 +202,7 @@ export async function planAppRoll(
|
|
|
166
202
|
): Promise<PlanResult> {
|
|
167
203
|
const services: ServicePlan[] = [];
|
|
168
204
|
for (const slug of ROLL_ORDER) {
|
|
169
|
-
const prefix =
|
|
205
|
+
const prefix = cloudRunServicePrefix(install.provider, install.env, slug);
|
|
170
206
|
const lookup = await resolveGcpResourceName(
|
|
171
207
|
run,
|
|
172
208
|
"run-service",
|
|
@@ -210,6 +246,7 @@ export async function planAppRoll(
|
|
|
210
246
|
liveVolumes: liveVolumes(json),
|
|
211
247
|
liveTimeout: liveTimeoutSeconds(json),
|
|
212
248
|
liveCpuIdle: liveCpuIdle(json),
|
|
249
|
+
liveDirectVpc: liveDirectVpc(json),
|
|
213
250
|
});
|
|
214
251
|
}
|
|
215
252
|
const env = planEnv(install, services, {});
|
|
@@ -265,29 +302,45 @@ export async function planAppRoll(
|
|
|
265
302
|
// stack's memory default (never lowered; an unparsable value is kept).
|
|
266
303
|
const memory = s.slug === "admin" ? planAdminMemory(s.liveMemory) : null;
|
|
267
304
|
if (memory && !memory.ok) warnings.push(`admin memory left as is: ${memory.error}`);
|
|
268
|
-
// #806 — the
|
|
269
|
-
//
|
|
305
|
+
// #806 — the MCP-Auth-Proxy streams remote-MCP chat turns
|
|
306
|
+
// (stack-contract.ts MCP_AUTH_PROXY_TIMEOUT_SECONDS). The other services'
|
|
307
|
+
// timeouts are the stacks' alone.
|
|
270
308
|
const timeout =
|
|
271
|
-
s.slug === "
|
|
309
|
+
s.slug === "mcp-auth-proxy"
|
|
310
|
+
? planRequestTimeout(s.liveTimeout, MCP_AUTH_PROXY_TIMEOUT_SECONDS)
|
|
311
|
+
: null;
|
|
272
312
|
const memoryFlags = memory?.ok ? memory.flags : [];
|
|
273
313
|
const timeoutFlags = timeout?.ok ? timeout.flags : [];
|
|
274
314
|
const cpu = planCpuIdle(s.liveCpuIdle, SERVICE_CPU_IDLE[s.slug]);
|
|
275
315
|
const cpuFlags = cpu.ok ? cpu.flags : [];
|
|
316
|
+
// #1328 — the gcp-firebase proxy is the former gateway service: it
|
|
317
|
+
// leaves the VPC in the same revision that drops its database env.
|
|
318
|
+
const vpc = SERVICE_VPC_EGRESS[s.slug] ? null : planNoVpc(s.liveDirectVpc);
|
|
319
|
+
const vpcFlags = vpc?.ok ? vpc.flags : [];
|
|
276
320
|
return {
|
|
277
321
|
...s,
|
|
278
322
|
envFlags: env.services[s.slug].flags,
|
|
279
323
|
envChanges: env.services[s.slug].changes,
|
|
280
|
-
resourceFlags: [...memoryFlags, ...timeoutFlags, ...cpuFlags],
|
|
324
|
+
resourceFlags: [...memoryFlags, ...timeoutFlags, ...cpuFlags, ...vpcFlags],
|
|
281
325
|
resourceChanges: [
|
|
282
326
|
...memoryFlags.map((f) => `${s.liveMemory ?? "(default)"} → ${f}`),
|
|
283
327
|
...(timeout?.ok ? timeout.changes : []),
|
|
284
328
|
...(cpu.ok ? cpu.changes : []),
|
|
329
|
+
...(vpc?.ok ? vpc.changes : []),
|
|
285
330
|
],
|
|
286
331
|
volumeFlags: [...(s.slug === "admin" ? media.flags : []), ...secretFiles[s.slug].flags],
|
|
287
332
|
volumeChanges: [...(s.slug === "admin" ? media.changes : []), ...secretFiles[s.slug].changes],
|
|
288
333
|
};
|
|
289
334
|
});
|
|
290
|
-
|
|
335
|
+
if (env.databaseHostPending) {
|
|
336
|
+
warnings.push(
|
|
337
|
+
"the services still dial the Cloud SQL instance's IP; the converge moves them onto its DNS name for verify-full TLS (#1338) before anything rolls",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
plan: { install, services: rolls, warnings, databaseHostPending: env.databaseHostPending },
|
|
343
|
+
};
|
|
291
344
|
}
|
|
292
345
|
|
|
293
346
|
/** Env the converge half adds to, or keeps out of, the rolls ({@link AppRollHooks.afterMigrations}). */
|
|
@@ -299,6 +352,11 @@ export interface EnvAdjustments {
|
|
|
299
352
|
* identity env, which only the converge (operator identity) can resolve.
|
|
300
353
|
*/
|
|
301
354
|
readonly extraEnv?: Readonly<Partial<Record<RollSlug, readonly CloudRunEnvVar[]>>>;
|
|
355
|
+
/**
|
|
356
|
+
* #1338 — the database host the converge moved the services onto (the
|
|
357
|
+
* Cloud SQL instance's DNS name, cloudsql-tls.ts).
|
|
358
|
+
*/
|
|
359
|
+
readonly databaseHost?: string;
|
|
302
360
|
}
|
|
303
361
|
|
|
304
362
|
function planEnv(
|
|
@@ -322,6 +380,7 @@ function planEnv(
|
|
|
322
380
|
{
|
|
323
381
|
leaveUntouched: adjust.leaveUntouched ?? [],
|
|
324
382
|
...(adjust.extraEnv ? { extraEnv: adjust.extraEnv } : {}),
|
|
383
|
+
...(adjust.databaseHost ? { databaseHost: adjust.databaseHost } : {}),
|
|
325
384
|
},
|
|
326
385
|
);
|
|
327
386
|
}
|
|
@@ -333,11 +392,12 @@ function planEnv(
|
|
|
333
392
|
*/
|
|
334
393
|
export function adjustingEnv(plan: AppRollPlan, adjust: EnvAdjustments): AppRollPlan {
|
|
335
394
|
const extra = Object.values(adjust.extraEnv ?? {}).some((v) => (v?.length ?? 0) > 0);
|
|
336
|
-
if ((adjust.leaveUntouched?.length ?? 0) === 0 && !extra) return plan;
|
|
395
|
+
if ((adjust.leaveUntouched?.length ?? 0) === 0 && !extra && !adjust.databaseHost) return plan;
|
|
337
396
|
const env = planEnv(plan.install, plan.services, adjust);
|
|
338
397
|
if (!env.ok) return plan;
|
|
339
398
|
return {
|
|
340
399
|
...plan,
|
|
400
|
+
databaseHostPending: env.databaseHostPending,
|
|
341
401
|
services: plan.services.map((s) => ({
|
|
342
402
|
...s,
|
|
343
403
|
envFlags: env.services[s.slug].flags,
|
|
@@ -370,7 +430,7 @@ export interface AppRollHooks {
|
|
|
370
430
|
afterMigrations?(): Promise<
|
|
371
431
|
| ({ readonly ok: true; readonly leaveUntouched: readonly string[] } & Pick<
|
|
372
432
|
EnvAdjustments,
|
|
373
|
-
"extraEnv"
|
|
433
|
+
"extraEnv" | "databaseHost"
|
|
374
434
|
>)
|
|
375
435
|
| { readonly ok: false }
|
|
376
436
|
>;
|
|
@@ -470,6 +530,17 @@ export async function executeAppRoll(
|
|
|
470
530
|
r.warn("--skip-health-check set — the new revisions get Cloud Run's readiness check only.");
|
|
471
531
|
}
|
|
472
532
|
|
|
533
|
+
// #1338 — the new images refuse a database URL without verify-full, and
|
|
534
|
+
// only a converge can move the services off the instance's IP. Without
|
|
535
|
+
// one (the in-cloud upgrader) the roll stops before it changes anything.
|
|
536
|
+
if (plan.databaseHostPending && !hooks.afterMigrations) {
|
|
537
|
+
r.error(
|
|
538
|
+
"The services still dial the Cloud SQL instance's IP address. This release connects with verify-full TLS, which needs the instance's DNS name: run the converge first (Settings → Updates installs it as a cloud-setup release, or `cms-provision upgrade` from a terminal).",
|
|
539
|
+
);
|
|
540
|
+
r.warn("No migrations ran and no traffic moved.");
|
|
541
|
+
return fail("migrations", false);
|
|
542
|
+
}
|
|
543
|
+
|
|
473
544
|
// Migrations BEFORE any traffic moves, with the NEW admin image: the old
|
|
474
545
|
// image does not carry the release's migration files (v0.9.2).
|
|
475
546
|
const admin = plan.services.find((s) => s.slug === "admin") as RollPlan;
|
|
@@ -490,8 +561,15 @@ export async function executeAppRoll(
|
|
|
490
561
|
plan = adjustingEnv(plan, {
|
|
491
562
|
leaveUntouched: converged.leaveUntouched,
|
|
492
563
|
...(converged.extraEnv ? { extraEnv: converged.extraEnv } : {}),
|
|
564
|
+
...(converged.databaseHost ? { databaseHost: converged.databaseHost } : {}),
|
|
493
565
|
});
|
|
494
566
|
}
|
|
567
|
+
if (plan.databaseHostPending) {
|
|
568
|
+
r.error(
|
|
569
|
+
"The converge did not move the services onto the Cloud SQL instance's DNS name, so the new revisions could not reach the database with verify-full TLS (#1338). Nothing rolled; fix the database TLS step above and re-run.",
|
|
570
|
+
);
|
|
571
|
+
return fail("converge", true);
|
|
572
|
+
}
|
|
495
573
|
|
|
496
574
|
const rolled: RollPlan[] = [];
|
|
497
575
|
/** Jobs this roll moved onto the admin's image (#875, #1217), with their prior image. */
|
|
@@ -540,10 +618,7 @@ export async function executeAppRoll(
|
|
|
540
618
|
region: install.region,
|
|
541
619
|
projectId: install.projectId,
|
|
542
620
|
imageRef: p.imageRef,
|
|
543
|
-
serviceAccount:
|
|
544
|
-
p.slug === "admin"
|
|
545
|
-
? runServiceAccountEmail(install.projectId, install.env)
|
|
546
|
-
: gatewayServiceAccountEmail(install.projectId, install.env),
|
|
621
|
+
serviceAccount: rollServiceAccount(p.slug, install.projectId, install.env),
|
|
547
622
|
envFlags: p.envFlags,
|
|
548
623
|
resourceFlags: p.resourceFlags,
|
|
549
624
|
volumeFlags: p.volumeFlags,
|
|
@@ -637,9 +712,12 @@ export async function executeAppRoll(
|
|
|
637
712
|
s.done(`Healthy: ${deep.detail}`);
|
|
638
713
|
}
|
|
639
714
|
|
|
715
|
+
const digestOf = (slug: RollSlug) =>
|
|
716
|
+
(plan.services.find((s) => s.slug === slug) as RollPlan).digest;
|
|
640
717
|
const digests: ImageDigests = {
|
|
641
|
-
admin: (
|
|
642
|
-
gateway: (
|
|
718
|
+
admin: digestOf("admin"),
|
|
719
|
+
gateway: digestOf("gateway"),
|
|
720
|
+
"mcp-auth-proxy": digestOf("mcp-auth-proxy"),
|
|
643
721
|
};
|
|
644
722
|
await deps.recordDigests(digests);
|
|
645
723
|
return { ok: true, digests };
|