@caelo-cms/provisioning 0.11.10 → 0.11.11
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/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adopt-existing.d.ts +6 -2
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +57 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +5 -2
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -1
- package/dist/agent/command.js.map +1 -1
- package/dist/agent-db.js +25 -8
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +59 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +30 -0
- package/dist/cloudsql-tls.d.ts.map +1 -1
- package/dist/cloudsql-tls.js +7 -1
- package/dist/cloudsql-tls.js.map +1 -1
- package/dist/compose.d.ts +14 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +54 -1
- package/dist/compose.js.map +1 -1
- package/dist/converge-db-trust.d.ts +44 -0
- package/dist/converge-db-trust.d.ts.map +1 -0
- package/dist/converge-db-trust.js +158 -0
- package/dist/converge-db-trust.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +5 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +9 -2
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +8 -0
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job.d.ts +17 -0
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +12 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/gcp-names.d.ts +13 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +4 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +4 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +7 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +3 -0
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +8 -3
- package/dist/migration-runner.js.map +1 -1
- package/dist/migration-targets.d.ts +11 -0
- package/dist/migration-targets.d.ts.map +1 -0
- package/dist/migration-targets.js +12 -0
- package/dist/migration-targets.js.map +1 -0
- package/dist/operator-access.d.ts +10 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +5 -0
- package/dist/operator-access.js.map +1 -1
- package/dist/pool-budget-stack.d.ts +13 -1
- package/dist/pool-budget-stack.d.ts.map +1 -1
- package/dist/pool-budget-stack.js +11 -1
- package/dist/pool-budget-stack.js.map +1 -1
- package/dist/public-role-isolation.d.ts +7 -3
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +25 -13
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/runtime-secrets.d.ts +2 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +19 -15
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +75 -0
- package/dist/scheduler-setup.d.ts.map +1 -0
- package/dist/scheduler-setup.js +331 -0
- package/dist/scheduler-setup.js.map +1 -0
- package/dist/self-hosted-roles.d.ts +16 -9
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +49 -15
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-scheduler-interval.d.ts +21 -0
- package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
- package/dist/self-hosted-scheduler-interval.js +68 -0
- package/dist/self-hosted-scheduler-interval.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +7 -4
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +9 -5
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +67 -6
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +87 -6
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-database.d.ts +22 -0
- package/dist/staging-database.d.ts.map +1 -0
- package/dist/staging-database.js +117 -0
- package/dist/staging-database.js.map +1 -0
- package/dist/upgrade/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +110 -10
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +6 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +3 -0
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +5 -4
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +5 -4
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -0
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +31 -0
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +81 -11
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +87 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-memory.test.ts +1 -1
- package/src/adopt-existing.test.ts +46 -0
- package/src/adopt-existing.ts +58 -2
- package/src/agent/command.ts +6 -3
- package/src/caddy-redirects.test.ts +1 -1
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +63 -8
- package/src/cloudsql-tls.test.ts +30 -0
- package/src/cloudsql-tls.ts +10 -3
- package/src/compose.ts +68 -0
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +9 -1
- package/src/converge-job-preflight.test.ts +5 -0
- package/src/converge-job-preflight.ts +8 -0
- package/src/converge-job.test.ts +70 -0
- package/src/converge-job.ts +32 -0
- package/src/database-tls-builders.test.ts +3 -2
- package/src/deployed-release.test.ts +3 -0
- package/src/gateway-env-isolation.test.ts +40 -7
- package/src/gcp-names.ts +25 -0
- package/src/index.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access.test.ts +15 -0
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +38 -13
- package/src/public-role-isolation.test.ts +18 -8
- package/src/public-role-isolation.ts +31 -17
- package/src/release-contract.ts +3 -0
- package/src/runtime-secrets.test.ts +18 -8
- package/src/runtime-secrets.ts +37 -18
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +72 -1
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +49 -19
- package/src/self-hosted-roles.ts +60 -20
- package/src/self-hosted-scheduler-interval.test.ts +66 -0
- package/src/self-hosted-scheduler-interval.ts +79 -0
- package/src/self-hosted-start.test.ts +1 -0
- package/src/self-hosted-upgrade.test.ts +2 -1
- package/src/self-hosted-upgrade.ts +14 -8
- package/src/stack-contract.test.ts +31 -3
- package/src/stack-contract.ts +113 -6
- package/src/stack-converge-secrets.test.ts +2 -0
- package/src/stack-converge.test.ts +6 -4
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/upgrade/converge.ts +130 -9
- package/src/upgrade/upgrade.test.ts +85 -2
- package/src/upgrade/upgrade.ts +9 -0
- package/src/upgrader-gcp.ts +5 -4
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +101 -11
- package/src/wizards/gcp.ts +109 -8
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +41 -1
- package/stacks/azure/index.ts +51 -3
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +50 -1
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +50 -1
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +4 -0
- package/stacks/self-hosted/index.ts +29 -4
package/src/wizards/gcp.ts
CHANGED
|
@@ -68,7 +68,11 @@ import {
|
|
|
68
68
|
REQUIRED_API_LIST,
|
|
69
69
|
serviceAccountExists,
|
|
70
70
|
} from "../gcloud.js";
|
|
71
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
GCP_STACK_ENV,
|
|
73
|
+
schedulerServiceAccountEmail,
|
|
74
|
+
upgraderServiceAccountEmail,
|
|
75
|
+
} from "../gcp-names.js";
|
|
72
76
|
import { INFRA_CONTRACT_VERSION } from "../infra-contract.js";
|
|
73
77
|
import {
|
|
74
78
|
INSTALL_JOB_NAME,
|
|
@@ -116,10 +120,12 @@ import {
|
|
|
116
120
|
resolveStackRemoteMcpIdentity,
|
|
117
121
|
} from "../remote-mcp.js";
|
|
118
122
|
import type { RunMode } from "../run-mode.js";
|
|
123
|
+
import { ensureSchedulerTick } from "../scheduler-setup.js";
|
|
119
124
|
import { selfCommand } from "../self-command.js";
|
|
120
125
|
import {
|
|
121
126
|
ABSENT_DEFAULT_FIREWALL_RULES,
|
|
122
127
|
cloudSqlPrivateDnsName,
|
|
128
|
+
parseSchedulerIntervalMinutes,
|
|
123
129
|
type RemoteMcpIdentity,
|
|
124
130
|
sameRemoteMcpIdentity,
|
|
125
131
|
} from "../stack-contract.js";
|
|
@@ -234,7 +240,15 @@ export async function runGcpWizard(opts: GcpWizardOpts): Promise<void> {
|
|
|
234
240
|
// drops the LB / Cloud CDN / Cloud Armor lines (saves ~$19/mo
|
|
235
241
|
// vs gcp). Default 'gcp' for backwards compatibility with
|
|
236
242
|
// installs that don't set the field.
|
|
237
|
-
const estimate = estimateGcpCost({
|
|
243
|
+
const estimate = estimateGcpCost({
|
|
244
|
+
...GCP_STACK_KNOB_DEFAULTS,
|
|
245
|
+
provider,
|
|
246
|
+
region,
|
|
247
|
+
// #1140 — the tick's line follows the knob the wizard recorded.
|
|
248
|
+
...(meta?.schedulerIntervalMinutes !== undefined
|
|
249
|
+
? { schedulerIntervalMinutes: meta.schedulerIntervalMinutes }
|
|
250
|
+
: {}),
|
|
251
|
+
});
|
|
238
252
|
note(
|
|
239
253
|
[
|
|
240
254
|
bold(
|
|
@@ -523,6 +537,18 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
523
537
|
// === 11.45. The converge job: releases that change the cloud setup (#819) ===
|
|
524
538
|
const convergeReady = await stepConvergeJob(projectId, region, releaseVersion);
|
|
525
539
|
|
|
540
|
+
// === 11.47. The scheduler tick: periodic work + plugin workers (#1140) ===
|
|
541
|
+
// Before operator access, which keeps the scheduler's account on IAP.
|
|
542
|
+
const schedulerReady = await stepSchedulerTick(
|
|
543
|
+
projectId,
|
|
544
|
+
region,
|
|
545
|
+
provider,
|
|
546
|
+
ownerEmail,
|
|
547
|
+
identity?.identity.adminUrl ?? null,
|
|
548
|
+
meta.schedulerIntervalMinutes,
|
|
549
|
+
);
|
|
550
|
+
const keepScheduler = schedulerReady || (await schedulerExists(projectId));
|
|
551
|
+
|
|
526
552
|
// === 11.5. Operator access: IAP follows the Caelo user list ===
|
|
527
553
|
await stepOperatorAccessSync(
|
|
528
554
|
projectId,
|
|
@@ -530,6 +556,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
530
556
|
provider,
|
|
531
557
|
ownerEmail,
|
|
532
558
|
keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : [],
|
|
559
|
+
keepScheduler ? [schedulerServiceAccountEmail(projectId, GCP_STACK_ENV)] : [],
|
|
533
560
|
);
|
|
534
561
|
|
|
535
562
|
// === 11.6. Remote MCP (#806): LB rules (gcp) + a check of the identity env ===
|
|
@@ -546,9 +573,10 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
546
573
|
// A fresh install ran every converge step above, so it is at this CLI's
|
|
547
574
|
// infrastructure contract — recorded only when the upgrader and the
|
|
548
575
|
// converge job are in place (the steps contract bumps 2 and 9 added) and
|
|
549
|
-
// remote MCP's steps (#806, bump 10)
|
|
550
|
-
// the admin can install the next
|
|
551
|
-
|
|
576
|
+
// remote MCP's steps (#806, bump 10) and the scheduler tick (#1140, bump
|
|
577
|
+
// 18) succeeded. With the release version, the admin can install the next
|
|
578
|
+
// update itself.
|
|
579
|
+
if (upgraderReady && convergeReady && remoteMcpReady && schedulerReady) {
|
|
552
580
|
const version = releaseVersion;
|
|
553
581
|
// #1025 — the linux/amd64 forms Cloud Run reports, next to the indexes,
|
|
554
582
|
// so the upgrader recognises what serves without a registry lookup.
|
|
@@ -998,7 +1026,7 @@ async function stepGrantBaseRoles(
|
|
|
998
1026
|
*/
|
|
999
1027
|
async function stepPublicRoleIsolation(projectId: string): Promise<void> {
|
|
1000
1028
|
const s = spinner();
|
|
1001
|
-
s.start("Removing
|
|
1029
|
+
s.start("Removing the gateway logins from the Cloud SQL superuser group...");
|
|
1002
1030
|
const r = await revokePublicRoleMemberships({
|
|
1003
1031
|
projectId,
|
|
1004
1032
|
env: GCP_STACK_ENV,
|
|
@@ -1010,7 +1038,7 @@ async function stepPublicRoleIsolation(projectId: string): Promise<void> {
|
|
|
1010
1038
|
cancel("Aborted.");
|
|
1011
1039
|
process.exit(1);
|
|
1012
1040
|
}
|
|
1013
|
-
s.stop(green("public_role
|
|
1041
|
+
s.stop(green("The gateway logins (public_role, public_staging_role) hold no database roles"));
|
|
1014
1042
|
}
|
|
1015
1043
|
|
|
1016
1044
|
/**
|
|
@@ -1216,6 +1244,78 @@ async function upgraderExists(projectId: string): Promise<boolean> {
|
|
|
1216
1244
|
return r.ok;
|
|
1217
1245
|
}
|
|
1218
1246
|
|
|
1247
|
+
/** Whether the scheduler tick's account exists from an earlier run (it keeps its IAP access). */
|
|
1248
|
+
async function schedulerExists(projectId: string): Promise<boolean> {
|
|
1249
|
+
const r = await gcloud([
|
|
1250
|
+
"iam",
|
|
1251
|
+
"service-accounts",
|
|
1252
|
+
"describe",
|
|
1253
|
+
schedulerServiceAccountEmail(projectId, GCP_STACK_ENV),
|
|
1254
|
+
`--project=${projectId}`,
|
|
1255
|
+
"--format=value(email)",
|
|
1256
|
+
]);
|
|
1257
|
+
return r.ok;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* #1140 — set up the scheduler tick (scheduler-setup.ts): the one Cloud
|
|
1262
|
+
* Scheduler job that calls the admin's tick every `schedulerIntervalMinutes`
|
|
1263
|
+
* through IAP. After migrations (the admin answers the tick once its tables
|
|
1264
|
+
* exist) and before operator access. Not fatal: the site works without it,
|
|
1265
|
+
* but plugin workers, update checks and clean-up do not run, so a failure
|
|
1266
|
+
* warns loudly and the install is not recorded at this contract — the next
|
|
1267
|
+
* wizard pass or `upgrade` retries. Idempotent.
|
|
1268
|
+
*
|
|
1269
|
+
* @returns whether the tick is in place
|
|
1270
|
+
*/
|
|
1271
|
+
async function stepSchedulerTick(
|
|
1272
|
+
projectId: string,
|
|
1273
|
+
region: string,
|
|
1274
|
+
provider: "gcp" | "gcp-firebase",
|
|
1275
|
+
ownerEmail: string,
|
|
1276
|
+
adminUrl: string | null,
|
|
1277
|
+
recordedInterval: number | undefined,
|
|
1278
|
+
): Promise<boolean> {
|
|
1279
|
+
const s = spinner();
|
|
1280
|
+
s.start("Setting up the scheduler tick (periodic work and plugin workers)...");
|
|
1281
|
+
const failed = (error: string) => {
|
|
1282
|
+
s.stop(yellow(`Scheduler tick not set up: ${error}`));
|
|
1283
|
+
log.warn(
|
|
1284
|
+
`The install works, but plugin workers (newsletter sends, rating aggregates), update checks and clean-up do not run until this succeeds. Re-run the installer or ${bold(selfCommand("upgrade"))} once the cause is fixed.`,
|
|
1285
|
+
);
|
|
1286
|
+
return false;
|
|
1287
|
+
};
|
|
1288
|
+
let intervalMinutes: number;
|
|
1289
|
+
try {
|
|
1290
|
+
intervalMinutes = parseSchedulerIntervalMinutes(
|
|
1291
|
+
recordedInterval,
|
|
1292
|
+
"install.json schedulerIntervalMinutes",
|
|
1293
|
+
);
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
return failed((e as Error).message);
|
|
1296
|
+
}
|
|
1297
|
+
if (!adminUrl) return failed("the admin's URL could not be read (see the remote-MCP step above)");
|
|
1298
|
+
const resolved = await resolveOperatorAccessTarget({
|
|
1299
|
+
provider,
|
|
1300
|
+
projectId,
|
|
1301
|
+
region,
|
|
1302
|
+
env: GCP_STACK_ENV,
|
|
1303
|
+
ownerEmail,
|
|
1304
|
+
});
|
|
1305
|
+
if (!resolved.ok) return failed(resolved.error);
|
|
1306
|
+
const r = await ensureSchedulerTick({
|
|
1307
|
+
projectId,
|
|
1308
|
+
region,
|
|
1309
|
+
env: GCP_STACK_ENV,
|
|
1310
|
+
adminUrl,
|
|
1311
|
+
resource: resolved.target.resource,
|
|
1312
|
+
intervalMinutes,
|
|
1313
|
+
});
|
|
1314
|
+
if (!r.ok) return failed(r.error);
|
|
1315
|
+
s.stop(green(`Scheduler tick ready (every ${intervalMinutes} min, ${r.done.length} steps)`));
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1219
1319
|
/**
|
|
1220
1320
|
* Set up the operator-access sync job (operator-access.ts): the only
|
|
1221
1321
|
* principal that may change who passes IAP, run as the operator's gcloud
|
|
@@ -1228,6 +1328,7 @@ async function stepOperatorAccessSync(
|
|
|
1228
1328
|
provider: "gcp" | "gcp-firebase",
|
|
1229
1329
|
ownerEmail: string,
|
|
1230
1330
|
mcpSigners: readonly string[],
|
|
1331
|
+
iapServiceAccounts: readonly string[],
|
|
1231
1332
|
): Promise<void> {
|
|
1232
1333
|
const s = spinner();
|
|
1233
1334
|
s.start("Setting up the operator-access sync job (Google IAP follows the user list)...");
|
|
@@ -1239,7 +1340,7 @@ async function stepOperatorAccessSync(
|
|
|
1239
1340
|
ownerEmail,
|
|
1240
1341
|
});
|
|
1241
1342
|
const r = resolved.ok
|
|
1242
|
-
? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners })
|
|
1343
|
+
? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners, iapServiceAccounts })
|
|
1243
1344
|
: { ok: false as const, done: [], error: resolved.error };
|
|
1244
1345
|
if (!r.ok) {
|
|
1245
1346
|
s.stop(red(`Failed: ${r.error}`));
|
package/stacks/aws/Pulumi.yaml
CHANGED
|
@@ -34,7 +34,7 @@ config:
|
|
|
34
34
|
description: RDS max_connections (parameter group); every service's pool size is computed from it (docs/dev/connection-budget.md).
|
|
35
35
|
caelo-aws:adminMaxInstances:
|
|
36
36
|
type: string
|
|
37
|
-
default: "
|
|
37
|
+
default: "4"
|
|
38
38
|
description: Most admin tasks at once; part of the connection budget.
|
|
39
39
|
caelo-aws:gatewayMaxInstances:
|
|
40
40
|
type: string
|
package/stacks/aws/index.ts
CHANGED
|
@@ -44,8 +44,10 @@ import {
|
|
|
44
44
|
} from "../../dist/aws-task-roles.js";
|
|
45
45
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
46
46
|
import {
|
|
47
|
+
DEFAULT_ADMIN_MAX_INSTANCES,
|
|
47
48
|
DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
48
49
|
DEFAULT_MAX_CONNECTIONS,
|
|
50
|
+
DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
49
51
|
stackPoolBudget,
|
|
50
52
|
} from "../../dist/pool-budget-stack.js";
|
|
51
53
|
import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../../dist/stack-contract.js";
|
|
@@ -86,7 +88,10 @@ const maxConnections = Number.parseInt(
|
|
|
86
88
|
cfg.get("maxConnections") ?? String(DEFAULT_MAX_CONNECTIONS.aws),
|
|
87
89
|
10,
|
|
88
90
|
);
|
|
89
|
-
const adminMaxInstances = Number.parseInt(
|
|
91
|
+
const adminMaxInstances = Number.parseInt(
|
|
92
|
+
cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
|
|
93
|
+
10,
|
|
94
|
+
);
|
|
90
95
|
const gatewayMaxInstances = Number.parseInt(
|
|
91
96
|
cfg.get("gatewayMaxInstances") ?? String(DEFAULT_GATEWAY_MAX_INSTANCES),
|
|
92
97
|
10,
|
|
@@ -95,6 +100,8 @@ const poolBudget = stackPoolBudget("aws", {
|
|
|
95
100
|
maxConnections,
|
|
96
101
|
adminMaxInstances,
|
|
97
102
|
gatewayMaxInstances,
|
|
103
|
+
// #1329 — the staging gateway joins the budget with the staging database.
|
|
104
|
+
stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
98
105
|
});
|
|
99
106
|
if (_adminMemoryMiB === null) {
|
|
100
107
|
throw new Error(`caelo-aws:adminMemory "${adminMemory}" is not a memory quantity (e.g. 2Gi)`);
|
|
@@ -131,6 +138,9 @@ const postgresPassword = pulumi.secret(randomHex(32));
|
|
|
131
138
|
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
132
139
|
// (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
|
|
133
140
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
141
|
+
// #1329 — the staging gateway's own login on cms_public_staging (created
|
|
142
|
+
// with the database by bootstrap.sh, PUBLIC_STAGING_ROLE_PASSWORD).
|
|
143
|
+
const publicStagingRolePassword = pulumi.secret(randomHex(32));
|
|
134
144
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
135
145
|
const cookieSecret = pulumi.secret(randomHex(32));
|
|
136
146
|
const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
|
|
@@ -151,6 +161,8 @@ function secret(name: string, value: pulumi.Output<string>): aws.secretsmanager.
|
|
|
151
161
|
|
|
152
162
|
const pgSecret = secret("postgres-password", postgresPassword);
|
|
153
163
|
const publicRoleSecret = secret("public-role-password", publicRolePassword);
|
|
164
|
+
// #1329 — read by nothing yet: the staging gateway's task (#749) will.
|
|
165
|
+
const _publicStagingRoleSecret = secret("public-staging-role-password", publicStagingRolePassword);
|
|
154
166
|
const csrfSecretRes = secret("csrf-secret", csrfSecret);
|
|
155
167
|
const cookieSecretRes = secret("cookie-secret", cookieSecret);
|
|
156
168
|
const anthropicSecretRes = secret("anthropic-api-key", anthropicApiKey);
|
|
@@ -213,6 +225,20 @@ const publicDatabaseUrl = pulumi
|
|
|
213
225
|
.apply(
|
|
214
226
|
([host, port, pw]) => `postgresql://public_role:${pw}@${host}:${port}/cms_public?${RDS_TLS}`,
|
|
215
227
|
);
|
|
228
|
+
// #1329 — the admin's pool on the staging database, and the staging
|
|
229
|
+
// gateway's only login (no task definition reads them before #749).
|
|
230
|
+
const publicStagingAdminDatabaseUrl = pulumi
|
|
231
|
+
.all([db.address, db.port, postgresPassword])
|
|
232
|
+
.apply(
|
|
233
|
+
([host, port, pw]) =>
|
|
234
|
+
`postgresql://caelo_admin:${pw}@${host}:${port}/cms_public_staging?${RDS_TLS}`,
|
|
235
|
+
);
|
|
236
|
+
const publicStagingDatabaseUrl = pulumi
|
|
237
|
+
.all([db.address, db.port, publicStagingRolePassword])
|
|
238
|
+
.apply(
|
|
239
|
+
([host, port, pw]) =>
|
|
240
|
+
`postgresql://public_staging_role:${pw}@${host}:${port}/cms_public_staging?${RDS_TLS}`,
|
|
241
|
+
);
|
|
216
242
|
|
|
217
243
|
// === 4. S3 buckets ===
|
|
218
244
|
const mediaBucket = new aws.s3.BucketV2(`${namePrefix}-media`, {
|
|
@@ -449,6 +475,16 @@ new aws.cloudwatch.LogDelivery(
|
|
|
449
475
|
// refuses every server-side render with that gap
|
|
450
476
|
// (capture/capture-backend.ts) instead of running a browser in its own task.
|
|
451
477
|
//
|
|
478
|
+
// FIXME(issue #749): no scheduler tick yet (#1140). On GCP one Cloud
|
|
479
|
+
// Scheduler job calls the admin's /api/internal/scheduler/tick every
|
|
480
|
+
// `schedulerIntervalMinutes` (src/scheduler-setup.ts), and the admin runs
|
|
481
|
+
// its periodic work and plugin workers only on that tick; self-hosted has a
|
|
482
|
+
// scheduler container. The AWS equivalent is one EventBridge Scheduler
|
|
483
|
+
// schedule that runs a small Fargate task (or Lambda) POSTing the same
|
|
484
|
+
// endpoint through the ALB's Cognito gate, with the same 1–15 minute knob.
|
|
485
|
+
// Nothing calls the tick here because no admin task exists on AWS yet; it
|
|
486
|
+
// lands with the admin task in #749.
|
|
487
|
+
//
|
|
452
488
|
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
453
489
|
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
454
490
|
// cloud-setup releases to the converge job; on AWS the admin refuses a
|
|
@@ -551,6 +587,7 @@ const dnsRecordsRequired: DnsRecord[] = [
|
|
|
551
587
|
const out: CloudAdapterOutputs = {
|
|
552
588
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
553
589
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
590
|
+
publicStagingDatabaseUrl: publicStagingDatabaseUrl as unknown as string,
|
|
554
591
|
mediaStorageUrl: pulumi.interpolate`s3://${mediaBucket.bucket}` as unknown as string,
|
|
555
592
|
mediaCdnBaseUrl:
|
|
556
593
|
pulumi.interpolate`https://${distribution.domainName}/media` as unknown as string,
|
|
@@ -565,6 +602,9 @@ const out: CloudAdapterOutputs = {
|
|
|
565
602
|
|
|
566
603
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
567
604
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
605
|
+
export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
|
|
606
|
+
// #1329 — what the admin task definition (#749) sets as PUBLIC_STAGING_ADMIN_DATABASE_URL.
|
|
607
|
+
export const publicStagingAdminDatabaseUrlOut = publicStagingAdminDatabaseUrl;
|
|
568
608
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
569
609
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
570
610
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
package/stacks/azure/index.ts
CHANGED
|
@@ -41,10 +41,15 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
41
41
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
42
42
|
import { azureFrontDoorDnsRecords } from "../../dist/azure-front-door.js";
|
|
43
43
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
DEFAULT_MAX_CONNECTIONS,
|
|
46
|
+
DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
47
|
+
stackPoolBudget,
|
|
48
|
+
} from "../../dist/pool-budget-stack.js";
|
|
45
49
|
import {
|
|
46
50
|
ADMIN_MEMORY_DEFAULT,
|
|
47
51
|
azureContainerAppResources,
|
|
52
|
+
GATEWAY_ENVIRONMENT_ENV,
|
|
48
53
|
GATEWAY_TRUSTED_PROXY_HOPS,
|
|
49
54
|
MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
|
|
50
55
|
REMOTE_MCP_MODE,
|
|
@@ -88,6 +93,8 @@ const poolBudget = stackPoolBudget("azure", {
|
|
|
88
93
|
maxConnections,
|
|
89
94
|
adminMaxInstances: ADMIN_SIDE_APPS * MAX_REPLICAS,
|
|
90
95
|
gatewayMaxInstances: MAX_REPLICAS,
|
|
96
|
+
// #1329 — the staging gateway joins the budget with the staging database.
|
|
97
|
+
stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
91
98
|
});
|
|
92
99
|
// #553 — the admin runs the Lighthouse quality audit; same knob + default
|
|
93
100
|
// as every adapter (stack-contract.ts). Container Apps only accepts fixed
|
|
@@ -116,6 +123,9 @@ const postgresPassword = pulumi.secret(randomHex(32));
|
|
|
116
123
|
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
117
124
|
// (ADMIN_ROLE_PASSWORD / PUBLIC_ROLE_PASSWORD).
|
|
118
125
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
126
|
+
// #1329 — the staging gateway's own login on cms_public_staging (created
|
|
127
|
+
// with the database by bootstrap.sh, PUBLIC_STAGING_ROLE_PASSWORD).
|
|
128
|
+
const publicStagingRolePassword = pulumi.secret(randomHex(32));
|
|
119
129
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
120
130
|
const cookieSecret = pulumi.secret(randomHex(32));
|
|
121
131
|
const anthropicApiKey = pulumi.secret(process.env.ANTHROPIC_API_KEY ?? "");
|
|
@@ -145,6 +155,8 @@ function keyVaultSecret(shortName: string, value: pulumi.Output<string>): azure.
|
|
|
145
155
|
|
|
146
156
|
const _pgSecret = keyVaultSecret("pg-password", postgresPassword);
|
|
147
157
|
keyVaultSecret("public-role-password", publicRolePassword);
|
|
158
|
+
// #1329 — read by nothing yet: the staging gateway's app (#750) will.
|
|
159
|
+
keyVaultSecret("public-staging-role-password", publicStagingRolePassword);
|
|
148
160
|
keyVaultSecret("csrf-secret", csrfSecret);
|
|
149
161
|
keyVaultSecret("cookie-secret", cookieSecret);
|
|
150
162
|
keyVaultSecret("anthropic-api-key", anthropicApiKey);
|
|
@@ -195,6 +207,13 @@ const _pgPublic = new azure.dbforpostgresql.Database(`${namePrefix}-cms-public-d
|
|
|
195
207
|
databaseName: "cms_public",
|
|
196
208
|
charset: "UTF8",
|
|
197
209
|
});
|
|
210
|
+
// #1329 — staging's own data, the twin of cms_public on the same server.
|
|
211
|
+
const _pgPublicStaging = new azure.dbforpostgresql.Database(`${namePrefix}-cms-public-staging-db`, {
|
|
212
|
+
resourceGroupName: rg.name,
|
|
213
|
+
serverName: pgServer.name,
|
|
214
|
+
databaseName: "cms_public_staging",
|
|
215
|
+
charset: "UTF8",
|
|
216
|
+
});
|
|
198
217
|
|
|
199
218
|
// #1338 — verify-full to the server's FQDN. Flexible Server's certificate chains
|
|
200
219
|
// to public roots (DigiCert Global Root G2, Microsoft RSA Root CA 2017) that
|
|
@@ -213,6 +232,19 @@ const publicAdminDatabaseUrl = pulumi
|
|
|
213
232
|
const publicDatabaseUrl = pulumi
|
|
214
233
|
.all([pgServer.fullyQualifiedDomainName, publicRolePassword])
|
|
215
234
|
.apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?${AZURE_TLS}`);
|
|
235
|
+
// #1329 — the admin's pool on the staging database (plugin schemas, the
|
|
236
|
+
// staging half of the projection), and the staging gateway's only login.
|
|
237
|
+
const publicStagingAdminDatabaseUrl = pulumi
|
|
238
|
+
.all([pgServer.fullyQualifiedDomainName, postgresPassword])
|
|
239
|
+
.apply(
|
|
240
|
+
([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public_staging?${AZURE_TLS}`,
|
|
241
|
+
);
|
|
242
|
+
const publicStagingDatabaseUrl = pulumi
|
|
243
|
+
.all([pgServer.fullyQualifiedDomainName, publicStagingRolePassword])
|
|
244
|
+
.apply(
|
|
245
|
+
([host, pw]) =>
|
|
246
|
+
`postgresql://public_staging_role:${pw}@${host}:5432/cms_public_staging?${AZURE_TLS}`,
|
|
247
|
+
);
|
|
216
248
|
|
|
217
249
|
// === 4. Storage account + containers ===
|
|
218
250
|
const storage = new azure.storage.StorageAccount(`${namePrefix}-st`, {
|
|
@@ -409,6 +441,15 @@ const logWorkspace = new azure.operationalinsights.Workspace(`${namePrefix}-logs
|
|
|
409
441
|
// refuses every server-side render with that gap
|
|
410
442
|
// (capture/capture-backend.ts) instead of running a browser in its own app.
|
|
411
443
|
//
|
|
444
|
+
// FIXME(issue #750): no scheduler tick yet (#1140). On GCP one Cloud
|
|
445
|
+
// Scheduler job calls the admin's /api/internal/scheduler/tick every
|
|
446
|
+
// `schedulerIntervalMinutes` (src/scheduler-setup.ts), and the admin runs
|
|
447
|
+
// its periodic work and plugin workers only on that tick; self-hosted has a
|
|
448
|
+
// scheduler container. The Azure equivalent is a scheduled Container Apps
|
|
449
|
+
// job (or Logic App recurrence) POSTing the same endpoint behind Easy Auth,
|
|
450
|
+
// with the same 1–15 minute knob. Nothing calls the tick here because no
|
|
451
|
+
// admin app exists on Azure yet; it lands with the admin in #750.
|
|
452
|
+
//
|
|
412
453
|
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
413
454
|
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
414
455
|
// cloud-setup releases to the converge job; on Azure the admin refuses a
|
|
@@ -503,6 +544,7 @@ function containerApp(args: ContainerAppArgs): azure.app.ContainerApp {
|
|
|
503
544
|
const adminDatabaseEnv = [
|
|
504
545
|
{ name: "ADMIN_DATABASE_URL", value: adminDatabaseUrl },
|
|
505
546
|
{ name: "PUBLIC_ADMIN_DATABASE_URL", value: publicAdminDatabaseUrl },
|
|
547
|
+
{ name: "PUBLIC_STAGING_ADMIN_DATABASE_URL", value: publicStagingAdminDatabaseUrl },
|
|
506
548
|
...poolBudget.env.admin,
|
|
507
549
|
];
|
|
508
550
|
const adminApp = containerApp({
|
|
@@ -519,8 +561,12 @@ const _gatewayApp = containerApp({
|
|
|
519
561
|
{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl },
|
|
520
562
|
...poolBudget.env.gateway,
|
|
521
563
|
],
|
|
522
|
-
// Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts)
|
|
523
|
-
|
|
564
|
+
// Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts),
|
|
565
|
+
// and (#1329) that this is the live site's gateway.
|
|
566
|
+
extraEnv: [
|
|
567
|
+
{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) },
|
|
568
|
+
{ name: GATEWAY_ENVIRONMENT_ENV, value: "live" },
|
|
569
|
+
],
|
|
524
570
|
});
|
|
525
571
|
// #1328 — the MCP-Auth-Proxy, its own Container App and so its own FQDN:
|
|
526
572
|
// remote MCP's sign-in pages never share an origin with the site's module
|
|
@@ -565,6 +611,7 @@ const dnsRecordsRequired = pulumi
|
|
|
565
611
|
const out: CloudAdapterOutputs = {
|
|
566
612
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
567
613
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
614
|
+
publicStagingDatabaseUrl: publicStagingDatabaseUrl as unknown as string,
|
|
568
615
|
mediaStorageUrl:
|
|
569
616
|
pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media` as unknown as string,
|
|
570
617
|
mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
|
|
@@ -578,6 +625,7 @@ const out: CloudAdapterOutputs = {
|
|
|
578
625
|
|
|
579
626
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
580
627
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
628
|
+
export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
|
|
581
629
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
582
630
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
583
631
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
package/stacks/gcp/Pulumi.yaml
CHANGED
|
@@ -55,7 +55,7 @@ config:
|
|
|
55
55
|
description: Admin Cloud Run memory. 2Gi leaves room for the Lighthouse quality audit next to editor requests; billed only while an instance runs.
|
|
56
56
|
caelo-gcp:adminMaxInstances:
|
|
57
57
|
type: string
|
|
58
|
-
default: "
|
|
58
|
+
default: "4"
|
|
59
59
|
description: |
|
|
60
60
|
Admin Cloud Run max instances. Each instance gets an equal share of
|
|
61
61
|
the connection budget (docs/dev/connection-budget.md): at the
|
package/stacks/gcp/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Pulumi stack provisioning Caelo on GCP managed services. Implements the shared `
|
|
|
12
12
|
| Container runtime | Four Cloud Run services (admin / gateway / orchestrator / runner) |
|
|
13
13
|
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret and public-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only `public-role-password` (#613, #635: the gateway has no cms_admin login). |
|
|
14
14
|
| Edge-log sink | Cloud Logging project sink → BigQuery dataset `<env>_edge_logs` (queryable by P12A analytics plugin) |
|
|
15
|
+
| Scheduler tick (CLI-owned, #1140) | One Cloud Scheduler job `caelo-<env>-scheduler-tick` starts the Workflows workflow of the same name every `schedulerIntervalMinutes` (1–15, default 5; `--scheduler-interval-minutes` on the installer, recorded in install.json). The workflow signs an IAP credential as `caelo-<env>-scheduler` and POSTs the admin's `/api/internal/scheduler/tick`, which runs the due periodic work and plugin workers. Set up by the installer and `upgrade` (`src/scheduler-setup.ts`), not by this stack. |
|
|
15
16
|
|
|
16
17
|
The Caelo runtime never knows it's on GCP — every Cloud Run service consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
|
|
17
18
|
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
} from "../../dist/gcp-names.js";
|
|
34
34
|
import { createSiteUrlMap } from "../../dist/gcp-url-map.js";
|
|
35
35
|
import {
|
|
36
|
+
DEFAULT_ADMIN_MAX_INSTANCES,
|
|
36
37
|
DEFAULT_GATEWAY_MAX_INSTANCES,
|
|
37
38
|
DEFAULT_MAX_CONNECTIONS,
|
|
39
|
+
DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
38
40
|
stackPoolBudget,
|
|
39
41
|
} from "../../dist/pool-budget-stack.js";
|
|
40
42
|
import {
|
|
@@ -119,7 +121,10 @@ const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
|
|
|
119
121
|
// against the original 25-slot Postgres default — connection exhaustion
|
|
120
122
|
// took the admin offline. v0.2.49 bakes the post-incident shape so fresh
|
|
121
123
|
// installs don't trip the same wall.
|
|
122
|
-
const adminMaxInstances = Number.parseInt(
|
|
124
|
+
const adminMaxInstances = Number.parseInt(
|
|
125
|
+
cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
|
|
126
|
+
10,
|
|
127
|
+
);
|
|
123
128
|
const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
|
|
124
129
|
// #1254 — the gateway's scale-out is part of the connection budget: each
|
|
125
130
|
// instance holds a cms_public pool.
|
|
@@ -137,6 +142,8 @@ const poolBudget = stackPoolBudget("gcp", {
|
|
|
137
142
|
maxConnections,
|
|
138
143
|
adminMaxInstances,
|
|
139
144
|
gatewayMaxInstances,
|
|
145
|
+
// #1329 — the staging gateway (P3b-2) is budgeted from this release on.
|
|
146
|
+
stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
|
|
140
147
|
});
|
|
141
148
|
const wafAdaptiveProtection = cfg.getBoolean("wafAdaptiveProtection") ?? false;
|
|
142
149
|
const backupRetentionDays = Number.parseInt(cfg.get("backupRetentionDays") ?? "7", 10);
|
|
@@ -281,6 +288,8 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
|
281
288
|
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
282
289
|
"staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
|
|
283
290
|
"public-role-password": gcpSecretId(env, "public-role-password"),
|
|
291
|
+
// #1329 — the staging gateway's login, never the live one's.
|
|
292
|
+
"public-staging-role-password": gcpSecretId(env, "public-staging-role-password"),
|
|
284
293
|
// #914 / #806 — the Owner-set Google sign-in client (a placeholder until
|
|
285
294
|
// then; IAP login, IAP programmatic access and the remote-MCP broker) and
|
|
286
295
|
// the broker's seal key, both CLI-generated.
|
|
@@ -297,6 +306,14 @@ const publicRolePassword = pulumi.secret(
|
|
|
297
306
|
{ provider: gcpProvider },
|
|
298
307
|
).secretData,
|
|
299
308
|
);
|
|
309
|
+
// #1329 — public_staging_role's own password (CLI-generated like the one
|
|
310
|
+
// above): the staging gateway's only login, on cms_public_staging only.
|
|
311
|
+
const publicStagingRolePassword = pulumi.secret(
|
|
312
|
+
gcp.secretmanager.getSecretVersionOutput(
|
|
313
|
+
{ project, secret: gcpSecretId(env, "public-staging-role-password") },
|
|
314
|
+
{ provider: gcpProvider },
|
|
315
|
+
).secretData,
|
|
316
|
+
);
|
|
300
317
|
|
|
301
318
|
// =========================================================================
|
|
302
319
|
// Tier 4 — Cloud SQL Postgres (private IP only; HA + retention configurable)
|
|
@@ -371,6 +388,14 @@ const pgPublicUser = new gcp.sql.User(
|
|
|
371
388
|
{ instance: sqlInstance.name, name: "public_role", password: publicRolePassword },
|
|
372
389
|
opts,
|
|
373
390
|
);
|
|
391
|
+
// #1329 — the staging gateway's login: an Admin API user like public_role,
|
|
392
|
+
// so the same membership revoke (public-role-isolation.ts) and the same
|
|
393
|
+
// audit apply; migration 0530 gives it CONNECT on cms_public_staging only.
|
|
394
|
+
new gcp.sql.User(
|
|
395
|
+
`${namePrefix}-pg-public-staging`,
|
|
396
|
+
{ instance: sqlInstance.name, name: "public_staging_role", password: publicStagingRolePassword },
|
|
397
|
+
opts,
|
|
398
|
+
);
|
|
374
399
|
|
|
375
400
|
const cmsAdminDb = new gcp.sql.Database(
|
|
376
401
|
`${namePrefix}-cms-admin-db`,
|
|
@@ -382,6 +407,13 @@ const cmsPublicDb = new gcp.sql.Database(
|
|
|
382
407
|
{ instance: sqlInstance.name, name: "cms_public" },
|
|
383
408
|
opts,
|
|
384
409
|
);
|
|
410
|
+
// #1329 — staging's own data: the twin of cms_public on the same instance
|
|
411
|
+
// (no extra cost), reached by the staging gateway only.
|
|
412
|
+
const cmsPublicStagingDb = new gcp.sql.Database(
|
|
413
|
+
`${namePrefix}-cms-public-staging-db`,
|
|
414
|
+
{ instance: sqlInstance.name, name: "cms_public_staging" },
|
|
415
|
+
opts,
|
|
416
|
+
);
|
|
385
417
|
|
|
386
418
|
// #1338 — the instance's DNS name, resolved to its private IP by a private
|
|
387
419
|
// zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
|
|
@@ -767,6 +799,8 @@ const envContractInputs = {
|
|
|
767
799
|
admin: dbUrls.admin,
|
|
768
800
|
publicAdmin: dbUrls.publicAdmin,
|
|
769
801
|
public: dbUrls.public,
|
|
802
|
+
publicStagingAdmin: dbUrls.publicStagingAdmin,
|
|
803
|
+
publicStaging: dbUrls.publicStaging,
|
|
770
804
|
},
|
|
771
805
|
} as const;
|
|
772
806
|
|
|
@@ -841,6 +875,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
841
875
|
pgAdminUser,
|
|
842
876
|
cmsAdminDb,
|
|
843
877
|
cmsPublicDb,
|
|
878
|
+
cmsPublicStagingDb,
|
|
844
879
|
// The env contract names the buckets as strings; keep them ordered
|
|
845
880
|
// before the services that use them.
|
|
846
881
|
mediaBucket,
|
|
@@ -1155,6 +1190,18 @@ new gcp.iap.WebBackendServiceIamMember(
|
|
|
1155
1190
|
// (packages/provisioning/src/capture-job-setup.ts), for the same reason as
|
|
1156
1191
|
// the audit job above.
|
|
1157
1192
|
|
|
1193
|
+
// =========================================================================
|
|
1194
|
+
// Scheduler tick — deliberately NOT here
|
|
1195
|
+
// =========================================================================
|
|
1196
|
+
//
|
|
1197
|
+
// #1140 — one Cloud Scheduler job starts a Cloud Workflows execution every
|
|
1198
|
+
// `schedulerIntervalMinutes` (1–15, default 5); the workflow signs an IAP
|
|
1199
|
+
// credential as the scheduler's own service account and POSTs the admin's
|
|
1200
|
+
// /api/internal/scheduler/tick, where the admin runs its periodic work and
|
|
1201
|
+
// plugin workers from its due-table. The CLI owns the account, its grants,
|
|
1202
|
+
// the workflow and the job (packages/provisioning/src/scheduler-setup.ts),
|
|
1203
|
+
// for the same reason as the audit job above.
|
|
1204
|
+
|
|
1158
1205
|
// =========================================================================
|
|
1159
1206
|
// Operator access — deliberately NOT here
|
|
1160
1207
|
// =========================================================================
|
|
@@ -1325,6 +1372,7 @@ const out: CloudAdapterOutputs = {
|
|
|
1325
1372
|
// Password-less — the password lives only in Secret Manager.
|
|
1326
1373
|
adminDatabaseUrl: dbUrls.admin as unknown as string,
|
|
1327
1374
|
publicDatabaseUrl: dbUrls.public as unknown as string,
|
|
1375
|
+
publicStagingDatabaseUrl: dbUrls.publicStaging as unknown as string,
|
|
1328
1376
|
mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
|
|
1329
1377
|
mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
|
|
1330
1378
|
bootstrapUrl:
|
|
@@ -1338,6 +1386,7 @@ const out: CloudAdapterOutputs = {
|
|
|
1338
1386
|
|
|
1339
1387
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
1340
1388
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
1389
|
+
export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
|
|
1341
1390
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
1342
1391
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
1343
1392
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
|
@@ -58,7 +58,7 @@ config:
|
|
|
58
58
|
description: Admin Cloud Run memory. 2Gi leaves room for the Lighthouse quality audit next to editor requests; billed only while an instance runs.
|
|
59
59
|
caelo-gcp-firebase:adminMaxInstances:
|
|
60
60
|
type: string
|
|
61
|
-
default: "
|
|
61
|
+
default: "4"
|
|
62
62
|
description: Admin Cloud Run max instances.
|
|
63
63
|
caelo-gcp-firebase:maxConnections:
|
|
64
64
|
type: string
|