@caelo-cms/provisioning 0.11.10 → 0.11.12
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 +196 -23
- 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 +12 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +42 -3
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +34 -2
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +76 -1
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job-setup.d.ts.map +1 -1
- package/dist/converge-job-setup.js +4 -1
- package/dist/converge-job-setup.js.map +1 -1
- package/dist/converge-job.d.ts +44 -3
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +105 -11
- 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 +10 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +10 -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/mcp-auth-proxy-setup.d.ts +3 -1
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +9 -4
- package/dist/mcp-auth-proxy-setup.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/upgrader-iam-preflight.d.ts +8 -6
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +5 -1
- package/dist/upgrader-iam-preflight.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 +82 -12
- 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 +42 -2
- package/src/converge-job-preflight.test.ts +106 -0
- package/src/converge-job-preflight.ts +97 -3
- package/src/converge-job-setup.ts +4 -1
- package/src/converge-job.test.ts +213 -0
- package/src/converge-job.ts +175 -19
- 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 +10 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- package/src/mcp-auth-proxy-setup.test.ts +33 -1
- package/src/mcp-auth-proxy-setup.ts +11 -4
- 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/upgrader-iam-preflight.ts +15 -10
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +102 -12
- 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-cost.ts
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* + AI calls + storage growth on top.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
ADMIN_MEMORY_DEFAULT,
|
|
16
|
+
memoryQuantityMiB,
|
|
17
|
+
parseSchedulerIntervalMinutes,
|
|
18
|
+
SERVICE_CPU_IDLE,
|
|
19
|
+
} from "../stack-contract.js";
|
|
15
20
|
|
|
16
21
|
export interface CostLine {
|
|
17
22
|
name: string;
|
|
@@ -41,6 +46,82 @@ export interface CostEstimateInputs {
|
|
|
41
46
|
provider?: "gcp" | "gcp-firebase";
|
|
42
47
|
/** #607 — the install region; shown in the table (prices are {@link PRICED_REGION} rates). */
|
|
43
48
|
region: string;
|
|
49
|
+
/** #1140 — the `schedulerIntervalMinutes` knob (1–15); undefined: the default. */
|
|
50
|
+
schedulerIntervalMinutes?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* #1140 — Workflows steps one scheduler tick costs (scheduler-setup.ts
|
|
55
|
+
* schedulerWorkflowSource): the claims `assign`, the signJwt call (a Google
|
|
56
|
+
* API, billed as an internal step), the `return` — and the POST to the
|
|
57
|
+
* admin, the one external HTTP step.
|
|
58
|
+
*/
|
|
59
|
+
export const SCHEDULER_TICK_STEPS = { internal: 3, external: 1 } as const;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Workflows list prices (USD, cloud.google.com/workflows/pricing, as of
|
|
63
|
+
* 2026-11) and the monthly free tier per billing account.
|
|
64
|
+
*/
|
|
65
|
+
const WORKFLOWS_PRICE = {
|
|
66
|
+
internalPer1000: 0.01,
|
|
67
|
+
externalPer1000: 0.025,
|
|
68
|
+
freeInternal: 5_000,
|
|
69
|
+
freeExternal: 2_000,
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
/** Cloud Scheduler: $0.10 per job a month; the first 3 per billing account are free. */
|
|
73
|
+
const SCHEDULER_JOB_USD = 0.1;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* #1140 — what the scheduler tick costs a month at `minutes` between ticks:
|
|
77
|
+
* the Cloud Scheduler job (priced as paid — the 3 free jobs are shared
|
|
78
|
+
* with every other project on the billing account) plus the Workflows
|
|
79
|
+
* steps past the free tier. What the tick costs on the admin's side is
|
|
80
|
+
* priced on the admin's own line ({@link adminKeptWarmByTicks}).
|
|
81
|
+
*/
|
|
82
|
+
export function schedulerTickMonthlyUsd(minutes: number): number {
|
|
83
|
+
const ticks = Math.ceil((30 * 24 * 60) / parseSchedulerIntervalMinutes(minutes));
|
|
84
|
+
const internal = Math.max(
|
|
85
|
+
0,
|
|
86
|
+
ticks * SCHEDULER_TICK_STEPS.internal - WORKFLOWS_PRICE.freeInternal,
|
|
87
|
+
);
|
|
88
|
+
const external = Math.max(
|
|
89
|
+
0,
|
|
90
|
+
ticks * SCHEDULER_TICK_STEPS.external - WORKFLOWS_PRICE.freeExternal,
|
|
91
|
+
);
|
|
92
|
+
const usd =
|
|
93
|
+
SCHEDULER_JOB_USD +
|
|
94
|
+
(internal / 1000) * WORKFLOWS_PRICE.internalPer1000 +
|
|
95
|
+
(external / 1000) * WORKFLOWS_PRICE.externalPer1000;
|
|
96
|
+
return Math.round(usd * 100) / 100;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cloud Run instance-based billing (CPU always allocated) list rates, USD,
|
|
101
|
+
* tier-1 region, cloud.google.com/run/pricing as of 2026-11. The free tier
|
|
102
|
+
* is left out: it is shared by every service on the billing account.
|
|
103
|
+
*/
|
|
104
|
+
const CLOUD_RUN_INSTANCE_PRICE = { vcpuSecond: 0.000018, gibSecond: 0.000002 } as const;
|
|
105
|
+
|
|
106
|
+
/** Seconds in the 30-day month the table prices. */
|
|
107
|
+
const MONTH_SECONDS = 30 * 24 * 60 * 60;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* #1140 — whether the scheduler tick keeps a scale-to-zero admin instance
|
|
111
|
+
* up around the clock. Cloud Run keeps an idle instance for up to 15
|
|
112
|
+
* minutes after its last request, and the tick comes at most every 15
|
|
113
|
+
* minutes, so with CPU always allocated (`SERVICE_CPU_IDLE.admin` false
|
|
114
|
+
* until #1361) the instance never stops and is billed every second. With
|
|
115
|
+
* request-billed CPU an idle instance costs nothing between ticks.
|
|
116
|
+
*/
|
|
117
|
+
export function adminKeptWarmByTicks(adminMinInstances: number): boolean {
|
|
118
|
+
return adminMinInstances === 0 && !SERVICE_CPU_IDLE.admin;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One always-on admin instance (1 vCPU, `gib` memory) a month at instance-based list rates. */
|
|
122
|
+
export function alwaysOnAdminInstanceUsd(gib: number): number {
|
|
123
|
+
const perSecond = CLOUD_RUN_INSTANCE_PRICE.vcpuSecond + gib * CLOUD_RUN_INSTANCE_PRICE.gibSecond;
|
|
124
|
+
return Math.round(perSecond * MONTH_SECONDS);
|
|
44
125
|
}
|
|
45
126
|
|
|
46
127
|
/** The region the price table's list rates are for. */
|
|
@@ -71,6 +152,7 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
71
152
|
const sqlMonthly = Math.round(sqlBase * sqlMultiplier);
|
|
72
153
|
|
|
73
154
|
const provider = inputs.provider ?? "gcp";
|
|
155
|
+
const schedulerMinutes = parseSchedulerIntervalMinutes(inputs.schedulerIntervalMinutes);
|
|
74
156
|
const adminMemory = inputs.adminMemory ?? ADMIN_MEMORY_DEFAULT;
|
|
75
157
|
const adminMiB = memoryQuantityMiB(adminMemory);
|
|
76
158
|
if (adminMiB === null) {
|
|
@@ -91,23 +173,26 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
91
173
|
},
|
|
92
174
|
{
|
|
93
175
|
name: `Cloud Run admin (${adminMemory})`,
|
|
94
|
-
// FIXME(issue #
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
176
|
+
// FIXME(issue #1361): with request-billed CPU (SERVICE_CPU_IDLE) the
|
|
177
|
+
// ticks stop keeping the instance up and this line drops to ~$7 of
|
|
178
|
+
// editing sessions. Until then the scheduler tick (#1140) keeps one
|
|
179
|
+
// instance up around the clock, billed at the always-on rate.
|
|
180
|
+
monthlyUsd: adminKeptWarmByTicks(inputs.adminMinInstances)
|
|
181
|
+
? alwaysOnAdminInstanceUsd(adminGiB)
|
|
182
|
+
: inputs.adminMinInstances === 0
|
|
99
183
|
? 7
|
|
100
184
|
: Math.round(1 + inputs.adminMinInstances * perMinInstanceUsd),
|
|
101
|
-
notes:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
185
|
+
notes: adminKeptWarmByTicks(inputs.adminMinInstances)
|
|
186
|
+
? `scale-to-zero, but CPU is always allocated and the scheduler tick (every ${schedulerMinutes} min) comes before Cloud Run's 15-minute idle shutdown, so one instance (1 vCPU, ${adminMemory}) runs around the clock until request-billed CPU lands (#1361)`
|
|
187
|
+
: inputs.adminMinInstances === 0
|
|
188
|
+
? "scale-to-zero, request-billed CPU; ~$7/mo light editorial use, ticks inside the free tier"
|
|
189
|
+
: `${inputs.adminMinInstances} min-instance${inputs.adminMinInstances > 1 ? "s" : ""} (no cold start); the scheduler tick adds nothing, the instance is up anyway`,
|
|
105
190
|
},
|
|
106
191
|
{
|
|
107
192
|
name: "Quality checks (Cloud Run audit job)",
|
|
108
193
|
monthlyUsd: 2,
|
|
109
194
|
notes:
|
|
110
|
-
"one container per Lighthouse run, all in parallel (up to 5 pages x 3 runs, ~90 s at 2 vCPU each): ~$0.05 per audited Stage, partly in the free tier; results bucket ~$0",
|
|
195
|
+
"one container per Lighthouse run, all in parallel (up to 5 pages x 3 runs, ~90 s at 2 vCPU each; a page whose runs disagree gets 2 more): ~$0.05 per audited Stage, at most ~$0.08, partly in the free tier; results bucket ~$0",
|
|
111
196
|
},
|
|
112
197
|
{
|
|
113
198
|
name: "Screenshots (Cloud Run capture job)",
|
|
@@ -192,7 +277,7 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
192
277
|
name: "Secret Manager (runtime secrets)",
|
|
193
278
|
monthlyUsd: 0.2,
|
|
194
279
|
notes:
|
|
195
|
-
"6 active versions free, then ~$0.06 each; the Google sign-in client (#914)
|
|
280
|
+
"6 active versions free, then ~$0.06 each; the Google sign-in client (#914), the remote-MCP seal key (#806) and the staging database login's password (#1329; the staging database itself shares the Cloud SQL instance at no extra cost)",
|
|
196
281
|
},
|
|
197
282
|
{
|
|
198
283
|
name: "BigQuery edge log sink",
|
|
@@ -211,6 +296,11 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
211
296
|
notes:
|
|
212
297
|
"Hourly ~20 s job keeps Google IAP in step with the user list; within the Cloud Run free tier, 1 of 3 free scheduler jobs",
|
|
213
298
|
},
|
|
299
|
+
{
|
|
300
|
+
name: `Scheduler tick (Cloud Scheduler + Workflows, every ${schedulerMinutes} min)`,
|
|
301
|
+
monthlyUsd: schedulerTickMonthlyUsd(schedulerMinutes),
|
|
302
|
+
notes: `one job starts a ${SCHEDULER_TICK_STEPS.internal + SCHEDULER_TICK_STEPS.external}-step workflow that calls the admin's periodic work and plugin workers; ~${Math.ceil((30 * 24 * 60) / schedulerMinutes)} ticks/mo; what the ticks cost on the admin is on the admin line`,
|
|
303
|
+
},
|
|
214
304
|
];
|
|
215
305
|
|
|
216
306
|
// Cents, so a sub-dollar line (the KMS key) doesn't print float noise.
|
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
|
|