@caelo-cms/provisioning 0.11.12 → 0.11.14
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 +4 -22
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -58
- package/dist/admin-domain.js.map +1 -1
- package/dist/agent-db.js +29 -5
- package/dist/audit-job-setup.d.ts.map +1 -1
- package/dist/audit-job-setup.js +14 -19
- package/dist/audit-job-setup.js.map +1 -1
- package/dist/caddy.d.ts +2 -1
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +21 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli.js +111 -62
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +22 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +53 -3
- 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 +1 -0
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +40 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +33 -14
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +43 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +19 -4
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +33 -5
- package/dist/gcp-url-map.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/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -29
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +8 -2
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +4 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -1
- package/dist/release-manifest.d.ts +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -0
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +11 -1
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +10 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +10 -1
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +5 -0
- package/dist/scheduler-setup.d.ts.map +1 -1
- package/dist/scheduler-setup.js +7 -0
- package/dist/scheduler-setup.js.map +1 -1
- package/dist/stack-contract.d.ts +61 -20
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +173 -35
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +4 -2
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +2 -2
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-gateway-setup.d.ts +140 -0
- package/dist/staging-gateway-setup.d.ts.map +1 -0
- package/dist/staging-gateway-setup.js +848 -0
- package/dist/staging-gateway-setup.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +4 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +7 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +36 -27
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +3 -3
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +2 -2
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +20 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +28 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +3 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +22 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +30 -5
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +2 -2
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +2 -2
- package/dist/upgrader-records.js.map +1 -1
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -3
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +48 -9
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-domain.test.ts +0 -91
- package/src/admin-domain.ts +4 -64
- package/src/admin-memory.test.ts +2 -0
- package/src/audit-job-setup.test.ts +11 -11
- package/src/audit-job-setup.ts +14 -19
- package/src/caddy-redirects.docker.test.ts +10 -5
- package/src/caddy-redirects.test.ts +7 -5
- package/src/caddy-validate.test.ts +1 -1
- package/src/caddy.test.ts +12 -6
- package/src/caddy.ts +23 -4
- package/src/cli.ts +138 -77
- package/src/compose.ts +67 -2
- package/src/converge-checkpoint.ts +1 -0
- package/src/database-tls-builders.test.ts +3 -1
- package/src/deployed-release.test.ts +21 -0
- package/src/deployed-release.ts +42 -7
- package/src/gateway-env-isolation.test.ts +2 -0
- package/src/gcp-names.ts +53 -0
- package/src/gcp-url-map.test.ts +26 -2
- package/src/gcp-url-map.ts +36 -5
- package/src/index.test.ts +9 -3
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/lifecycle.ts +6 -40
- package/src/mcp-auth-proxy-setup.test.ts +15 -6
- package/src/mcp-auth-proxy-setup.ts +14 -2
- package/src/release-manifest.ts +3 -1
- package/src/remote-mcp-stack-config.test.ts +1 -0
- package/src/remote-mcp.test.ts +5 -1
- package/src/remote-mcp.ts +10 -1
- package/src/runtime-secrets.test.ts +10 -1
- package/src/runtime-secrets.ts +21 -0
- package/src/scheduler-setup.test.ts +6 -0
- package/src/scheduler-setup.ts +7 -0
- package/src/self-hosted-backup.test.ts +6 -4
- package/src/self-hosted-compose.test.ts +2 -0
- package/src/stack-contract-parity.test.ts +7 -3
- package/src/stack-contract.test.ts +73 -13
- package/src/stack-contract.ts +195 -42
- package/src/stack-converge-secrets.test.ts +7 -2
- package/src/stack-converge.test.ts +37 -9
- package/src/stack-converge.ts +4 -1
- package/src/staging-admin.ts +2 -2
- package/src/staging-gateway-setup.test.ts +361 -0
- package/src/staging-gateway-setup.ts +1083 -0
- package/src/upgrade/app-roll.ts +7 -4
- package/src/upgrade/converge.ts +40 -26
- package/src/upgrade/image-forms.ts +7 -7
- package/src/upgrade/release-images.test.ts +28 -0
- package/src/upgrade/release-images.ts +44 -0
- package/src/upgrade/upgrade.test.ts +63 -11
- package/src/upgrade/upgrade.ts +29 -2
- package/src/upgrader-job.test.ts +23 -0
- package/src/upgrader-job.ts +45 -11
- package/src/upgrader-records.ts +7 -7
- package/src/wizard.ts +2 -3
- package/src/wizards/gcp.ts +73 -9
- package/stacks/gcp/index.ts +47 -6
- package/stacks/gcp-firebase/index.ts +44 -12
- package/stacks/self-hosted/index.ts +9 -2
package/src/compose.ts
CHANGED
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
publicSiteUrl,
|
|
46
46
|
REMOTE_MCP_MODE,
|
|
47
47
|
REMOTE_MCP_MODE_ENV,
|
|
48
|
+
STAGING_GATEWAY_TRUSTED_PROXY_HOPS,
|
|
49
|
+
STAGING_STORAGE_ENV,
|
|
48
50
|
TRUSTED_PROXY_HOPS_ENV,
|
|
49
51
|
} from "./stack-contract.js";
|
|
50
52
|
import { ghcrRepository, isImageDigest, type ReleaseService } from "./upgrade/release-images.js";
|
|
@@ -120,6 +122,18 @@ export const MEDIA_ROOT = "/data/media";
|
|
|
120
122
|
/** The gateway's HTTP port inside the Compose network. */
|
|
121
123
|
export const COMPOSE_GATEWAY_PORT = 8090;
|
|
122
124
|
|
|
125
|
+
/** #1330 — the staging gateway's HTTP port inside the Compose network. */
|
|
126
|
+
export const COMPOSE_STAGING_GATEWAY_PORT = 8091;
|
|
127
|
+
|
|
128
|
+
/** #1330 — the Compose service name of the staging gateway. */
|
|
129
|
+
export const COMPOSE_STAGING_GATEWAY_SERVICE = "caelo-staging-gateway";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* #1330 — where the staging gateway sees the staged builds: the admin's
|
|
133
|
+
* `output/staging` (host `.caelo/output/output/staging`), mounted read-only.
|
|
134
|
+
*/
|
|
135
|
+
export const STAGING_GATEWAY_STORAGE_DIR = "/srv/staging";
|
|
136
|
+
|
|
123
137
|
/** #1328 — the MCP-Auth-Proxy's HTTP port inside the Compose network. */
|
|
124
138
|
export const COMPOSE_MCP_AUTH_PROXY_PORT = 8095;
|
|
125
139
|
|
|
@@ -130,12 +144,17 @@ export const COMPOSE_MCP_AUTH_PROXY_SERVICE = "caelo-mcp-auth-proxy";
|
|
|
130
144
|
* The Compose services that touch the databases — what `backup` and
|
|
131
145
|
* `restore` stop while they read or rewrite them. Workers run inside the
|
|
132
146
|
* admin (CLAUDE.md §11.B Tier 5); there is no orchestrator or runner
|
|
133
|
-
* service. The
|
|
147
|
+
* service. The staging gateway (#1330) holds the staging database's login.
|
|
148
|
+
* The MCP-Auth-Proxy (#1328) is deliberately not here: it holds no
|
|
134
149
|
* database login, so it keeps running, and a release from before it has no
|
|
135
150
|
* such service for `up` to start. Its health is checked wherever the
|
|
136
151
|
* install starts (self-hosted-start.ts) or rolls (self-hosted-upgrade.ts).
|
|
137
152
|
*/
|
|
138
|
-
export const COMPOSE_APP_SERVICES = [
|
|
153
|
+
export const COMPOSE_APP_SERVICES = [
|
|
154
|
+
"caelo-admin",
|
|
155
|
+
"caelo-gateway",
|
|
156
|
+
COMPOSE_STAGING_GATEWAY_SERVICE,
|
|
157
|
+
] as const;
|
|
139
158
|
|
|
140
159
|
/**
|
|
141
160
|
* Where Caddy reaches the admin and the gateway: their Compose service
|
|
@@ -144,6 +163,7 @@ export const COMPOSE_APP_SERVICES = ["caelo-admin", "caelo-gateway"] as const;
|
|
|
144
163
|
*/
|
|
145
164
|
export const COMPOSE_ADMIN_UPSTREAM = `caelo-admin:${COMPOSE_ADMIN_PORT}`;
|
|
146
165
|
export const COMPOSE_GATEWAY_UPSTREAM = `caelo-gateway:${COMPOSE_GATEWAY_PORT}`;
|
|
166
|
+
export const COMPOSE_STAGING_GATEWAY_UPSTREAM = `${COMPOSE_STAGING_GATEWAY_SERVICE}:${COMPOSE_STAGING_GATEWAY_PORT}`;
|
|
147
167
|
export const COMPOSE_MCP_AUTH_PROXY_UPSTREAM = `${COMPOSE_MCP_AUTH_PROXY_SERVICE}:${COMPOSE_MCP_AUTH_PROXY_PORT}`;
|
|
148
168
|
|
|
149
169
|
/**
|
|
@@ -226,6 +246,15 @@ export interface RolePasswords {
|
|
|
226
246
|
|
|
227
247
|
export interface ComposeSpec {
|
|
228
248
|
readonly domain: string;
|
|
249
|
+
/** #650 — the staging site's host; seeds the admin's staging host setting. */
|
|
250
|
+
readonly stagingHost: string;
|
|
251
|
+
/**
|
|
252
|
+
* #650 / #1330 — the staging gate's key (`.caelo/config.json`
|
|
253
|
+
* `stagingGateSecret`, generated once): signs "Open staging" links and
|
|
254
|
+
* derives the crawler credentials. Admin only; the admin publishes it to
|
|
255
|
+
* the staging gateway through the staging projection.
|
|
256
|
+
*/
|
|
257
|
+
readonly stagingGateSecret: string;
|
|
229
258
|
/** The Postgres superuser's; only the database container uses it. */
|
|
230
259
|
readonly postgresPassword: string; // generated by cms-provision; persisted in .caelo/secrets
|
|
231
260
|
readonly rolePasswords: RolePasswords;
|
|
@@ -378,6 +407,8 @@ ${postgresAgent(spec.agent)} volumes:
|
|
|
378
407
|
${adminService(spec, adminMemoryMiB, env)}
|
|
379
408
|
|
|
380
409
|
${gatewayService(spec)}
|
|
410
|
+
|
|
411
|
+
${stagingGatewayService(spec)}
|
|
381
412
|
${mcpAuthProxyService(spec.images)}
|
|
382
413
|
${schedulerService(spec)}
|
|
383
414
|
volumes:
|
|
@@ -475,6 +506,12 @@ ${serviceRuntime(
|
|
|
475
506
|
PUBLIC_STAGING_ADMIN_DATABASE_URL: "${dbUrl("admin_role", spec.rolePasswords.admin, "cms_public_staging")}"
|
|
476
507
|
CAELO_SECRET_KEK: "${escapeYaml(spec.caeloSecretKek)}"
|
|
477
508
|
CAELO_INTERNAL_SECRET: "${escapeYaml(spec.internalSecret)}"
|
|
509
|
+
# #650 / #1330 — the staging gate's key and the staging host the admin seeds.
|
|
510
|
+
CAELO_STAGING_GATE_SECRET: "${escapeYaml(spec.stagingGateSecret)}"
|
|
511
|
+
CAELO_STAGING_HOST: "${escapeYaml(spec.stagingHost)}"
|
|
512
|
+
# #1330 — the staging gateway on the Compose network: the Stage's
|
|
513
|
+
# serve check asks it whether it sees the build just written.
|
|
514
|
+
CAELO_STAGING_GATEWAY_URL: "http://${COMPOSE_STAGING_GATEWAY_UPSTREAM}"
|
|
478
515
|
CAELO_SITE_URL: "${escapeYaml(selfHostedSiteUrl(spec.domain, spec.ports.https))}"
|
|
479
516
|
PORT: "${COMPOSE_ADMIN_PORT}"
|
|
480
517
|
CAELO_OUTPUT_ROOT: "${SITE_OUTPUT_ROOT}"
|
|
@@ -505,6 +542,34 @@ ${poolEnvLines("gateway")}
|
|
|
505
542
|
NODE_ENV: production`;
|
|
506
543
|
}
|
|
507
544
|
|
|
545
|
+
/**
|
|
546
|
+
* #1330 — the staging gateway: the gateway's image with
|
|
547
|
+
* `CAELO_GATEWAY_ENVIRONMENT=staging`. It serves the staged builds, which it
|
|
548
|
+
* reads from the admin's `output/staging` mounted read-only (no other part
|
|
549
|
+
* of the output, nothing writable), behind the staging gate, and staging's
|
|
550
|
+
* own data: `public_staging_role` on `cms_public_staging` is its only
|
|
551
|
+
* login. Caddy sends every request for the staging host here.
|
|
552
|
+
*/
|
|
553
|
+
function stagingGatewayService(spec: ComposeSpec): string {
|
|
554
|
+
return ` ${COMPOSE_STAGING_GATEWAY_SERVICE}:
|
|
555
|
+
${serviceRuntime(spec.images, "gateway", "bun run apps/api-gateway/src/server.ts", [
|
|
556
|
+
DATABASE_CA_MOUNT,
|
|
557
|
+
`./output/output/staging:${STAGING_GATEWAY_STORAGE_DIR}:ro`,
|
|
558
|
+
])}
|
|
559
|
+
restart: unless-stopped
|
|
560
|
+
depends_on:
|
|
561
|
+
postgres: { condition: service_healthy }
|
|
562
|
+
environment:
|
|
563
|
+
PUBLIC_DATABASE_URL: "${dbUrl("public_staging_role", spec.rolePasswords.publicStaging, "cms_public_staging")}"
|
|
564
|
+
${GATEWAY_ENVIRONMENT_ENV}: "staging"
|
|
565
|
+
GATEWAY_PORT: "${COMPOSE_STAGING_GATEWAY_PORT}"
|
|
566
|
+
${STAGING_STORAGE_ENV}: "file://${STAGING_GATEWAY_STORAGE_DIR}"
|
|
567
|
+
# Caddy is the one proxy in front (client-ip.ts in the gateway).
|
|
568
|
+
${TRUSTED_PROXY_HOPS_ENV}: "${STAGING_GATEWAY_TRUSTED_PROXY_HOPS["self-hosted"]}"
|
|
569
|
+
${poolEnvLines("gateway")}
|
|
570
|
+
NODE_ENV: production`;
|
|
571
|
+
}
|
|
572
|
+
|
|
508
573
|
/**
|
|
509
574
|
* #1328 — the MCP-Auth-Proxy: remote MCP's sign-in broker, its own
|
|
510
575
|
* container so it never shares a process with visitor handlers. Remote MCP
|
|
@@ -49,6 +49,7 @@ export const ConvergeCheckpointSchema = InstallJobStatusSchema.extend({
|
|
|
49
49
|
admin: z.string().min(1),
|
|
50
50
|
gateway: z.string().min(1),
|
|
51
51
|
// #1328 — absent in a checkpoint an older release wrote.
|
|
52
|
+
"staging-gateway": z.string().min(1).optional(),
|
|
52
53
|
"mcp-auth-proxy": z.string().min(1).optional(),
|
|
53
54
|
})
|
|
54
55
|
.optional(),
|
|
@@ -32,6 +32,8 @@ describe("database URL builders (#1338)", () => {
|
|
|
32
32
|
caeloSecretKek: "0".repeat(64),
|
|
33
33
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
34
34
|
internalSecret: "i".repeat(48),
|
|
35
|
+
stagingHost: "staging.example.com",
|
|
36
|
+
stagingGateSecret: "g".repeat(64),
|
|
35
37
|
diskSize: "20Gi",
|
|
36
38
|
images: {
|
|
37
39
|
mode: "release",
|
|
@@ -43,7 +45,7 @@ describe("database URL builders (#1338)", () => {
|
|
|
43
45
|
});
|
|
44
46
|
const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
|
|
45
47
|
// admin: cms_admin, cms_public, cms_public_staging (#1329); gateway: cms_public.
|
|
46
|
-
expect(urls).toHaveLength(
|
|
48
|
+
expect(urls).toHaveLength(5);
|
|
47
49
|
for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
|
|
48
50
|
});
|
|
49
51
|
|
|
@@ -111,6 +111,27 @@ describe("readDeployedImageDigests", () => {
|
|
|
111
111
|
if (!r.ok) expect(r.error).toContain("not pinned to a digest");
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it("#1330 — accepts a staging gateway on the gateway's image, refuses one on another", async () => {
|
|
115
|
+
const answers = (staging: string) => ({
|
|
116
|
+
...firebaseLists(),
|
|
117
|
+
"metadata.name~^caelo-production-staging-gateway": [ok("caelo-production-staging-gateway\n")],
|
|
118
|
+
"describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
|
|
119
|
+
"describe caelo-production-data-gateway": [ok(`${REG}/gateway@${B}`)],
|
|
120
|
+
"describe caelo-production-gateway-def": [ok(`${REG}/mcp-auth-proxy@${C}`)],
|
|
121
|
+
"describe caelo-production-staging-gateway": [ok(`${REG}/gateway@${staging}`)],
|
|
122
|
+
});
|
|
123
|
+
const same = fakeGcloud(answers(B));
|
|
124
|
+
expect(await readDeployedImageDigests({ ...FIREBASE, run: same.run })).toEqual({
|
|
125
|
+
ok: true,
|
|
126
|
+
digests: { admin: A, gateway: B, "mcp-auth-proxy": C },
|
|
127
|
+
});
|
|
128
|
+
expect(same.calls.some((c) => c.includes("caelo-production-staging-gateway"))).toBe(true);
|
|
129
|
+
const other = fakeGcloud(answers(A));
|
|
130
|
+
const r = await readDeployedImageDigests({ ...FIREBASE, run: other.run });
|
|
131
|
+
expect(r.ok).toBe(false);
|
|
132
|
+
if (!r.ok) expect(r.error).toContain("the staging gateway runs");
|
|
133
|
+
});
|
|
134
|
+
|
|
114
135
|
it("surfaces a gcloud failure", async () => {
|
|
115
136
|
const { run } = fakeGcloud({ "services list": [fail("PERMISSION_DENIED")] });
|
|
116
137
|
const r = await readDeployedImageDigests({ ...FIREBASE, run });
|
package/src/deployed-release.ts
CHANGED
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
cloudRunServicePrefix,
|
|
22
22
|
type GcpProvider,
|
|
23
23
|
RETIRED_SERVICE_ENV,
|
|
24
|
+
SERVICE_IMAGE_NAMES,
|
|
24
25
|
SERVICE_SECRET_ENV,
|
|
26
|
+
type ServiceImageName,
|
|
25
27
|
} from "./stack-contract.js";
|
|
26
28
|
import { liveContainerEnv } from "./stack-converge.js";
|
|
27
29
|
|
|
@@ -35,7 +37,7 @@ async function findDeployedService(
|
|
|
35
37
|
run: GcloudRunner,
|
|
36
38
|
opts: { provider: GcpProvider; projectId: string; region: string },
|
|
37
39
|
slug: CloudRunSlug,
|
|
38
|
-
): Promise<{ ok: true; name: string } | { ok: false; error: string }> {
|
|
40
|
+
): Promise<{ ok: true; name: string } | { ok: false; error: string; missing: boolean }> {
|
|
39
41
|
const prefix = cloudRunServicePrefix(opts.provider, GCP_STACK_ENV, slug);
|
|
40
42
|
const list = await run([
|
|
41
43
|
"run",
|
|
@@ -46,7 +48,8 @@ async function findDeployedService(
|
|
|
46
48
|
`--filter=metadata.name~^${prefix}`,
|
|
47
49
|
"--format=value(metadata.name)",
|
|
48
50
|
]);
|
|
49
|
-
if (!list.ok)
|
|
51
|
+
if (!list.ok)
|
|
52
|
+
return { ok: false, error: `list Cloud Run services: ${list.stderr.trim()}`, missing: false };
|
|
50
53
|
const names = list.stdout
|
|
51
54
|
.split("\n")
|
|
52
55
|
.map((s) => s.trim())
|
|
@@ -58,6 +61,7 @@ async function findDeployedService(
|
|
|
58
61
|
names.length === 0
|
|
59
62
|
? `no ${prefix}* Cloud Run service: the install runs a release from before it (the ${slug} became its own service in #1328). Run \`upgrade\` first: it creates the service and moves the release over.`
|
|
60
63
|
: `expected one ${prefix}* Cloud Run service, found ${names.join(", ")}`,
|
|
64
|
+
missing: names.length === 0,
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
return { ok: true, name: names[0] as string };
|
|
@@ -86,7 +90,11 @@ export async function checkDeployedRuntimeEnv(opts: {
|
|
|
86
90
|
const run = opts.run ?? defaultGcloud;
|
|
87
91
|
for (const slug of CLOUD_RUN_SLUGS) {
|
|
88
92
|
const found = await findDeployedService(run, opts, slug);
|
|
89
|
-
|
|
93
|
+
// #1330 — the staging gateway is the CLI's (staging-gateway-setup.ts):
|
|
94
|
+
// an install from before it gets it after this `up`, so a missing one
|
|
95
|
+
// runs no release that could mismatch the env.
|
|
96
|
+
if (!found.ok && slug === "staging-gateway" && found.missing) continue;
|
|
97
|
+
if (!found.ok) return { ok: false, error: found.error };
|
|
90
98
|
const describe = await run([
|
|
91
99
|
"run",
|
|
92
100
|
"services",
|
|
@@ -131,8 +139,14 @@ export async function readDeployedImageDigests(opts: {
|
|
|
131
139
|
run?: GcloudRunner;
|
|
132
140
|
}): Promise<{ ok: true; digests: ImageDigests } | { ok: false; error: string }> {
|
|
133
141
|
const run = opts.run ?? defaultGcloud;
|
|
134
|
-
const digests: Record<
|
|
135
|
-
|
|
142
|
+
const digests: Record<ServiceImageName, string> = {
|
|
143
|
+
admin: "",
|
|
144
|
+
gateway: "",
|
|
145
|
+
"mcp-auth-proxy": "",
|
|
146
|
+
};
|
|
147
|
+
const digestOf = async (
|
|
148
|
+
slug: CloudRunSlug,
|
|
149
|
+
): Promise<{ ok: true; digest: string } | { ok: false; error: string; missing: boolean }> => {
|
|
136
150
|
const found = await findDeployedService(run, opts, slug);
|
|
137
151
|
if (!found.ok) return found;
|
|
138
152
|
const image = await run([
|
|
@@ -144,15 +158,36 @@ export async function readDeployedImageDigests(opts: {
|
|
|
144
158
|
`--project=${opts.projectId}`,
|
|
145
159
|
"--format=value(spec.template.spec.containers[0].image)",
|
|
146
160
|
]);
|
|
147
|
-
if (!image.ok)
|
|
161
|
+
if (!image.ok) {
|
|
162
|
+
return { ok: false, error: `describe ${found.name}: ${image.stderr.trim()}`, missing: false };
|
|
163
|
+
}
|
|
148
164
|
const digest = digestFromImageRef(image.stdout);
|
|
149
165
|
if (!digest) {
|
|
150
166
|
return {
|
|
151
167
|
ok: false,
|
|
152
168
|
error: `${found.name} runs '${image.stdout.trim()}', which is not pinned to a digest`,
|
|
169
|
+
missing: false,
|
|
153
170
|
};
|
|
154
171
|
}
|
|
155
|
-
|
|
172
|
+
return { ok: true, digest };
|
|
173
|
+
};
|
|
174
|
+
// One digest per release image.
|
|
175
|
+
for (const slug of SERVICE_IMAGE_NAMES) {
|
|
176
|
+
const d = await digestOf(slug);
|
|
177
|
+
if (!d.ok) return { ok: false, error: d.error };
|
|
178
|
+
digests[slug] = d.digest;
|
|
179
|
+
}
|
|
180
|
+
// #1330 — the staging gateway runs the gateway's image and rolls with it:
|
|
181
|
+
// one that runs another image means the services serve two releases, and
|
|
182
|
+
// the wizard must not record (or deploy) either as "the" release. An
|
|
183
|
+
// install from before it has none yet (the CLI creates it after `up`).
|
|
184
|
+
const staging = await digestOf("staging-gateway");
|
|
185
|
+
if (!staging.ok && !staging.missing) return { ok: false, error: staging.error };
|
|
186
|
+
if (staging.ok && staging.digest !== digests.gateway) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: `the staging gateway runs ${staging.digest}, but the gateway runs ${digests.gateway}: the two must run the same release image. Run \`upgrade\` first — it rolls both onto one release — then re-run the installer.`,
|
|
190
|
+
};
|
|
156
191
|
}
|
|
157
192
|
return { ok: true, digests };
|
|
158
193
|
}
|
|
@@ -162,6 +162,8 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
162
162
|
caeloSecretKek: "0".repeat(64),
|
|
163
163
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
164
164
|
internalSecret: "i".repeat(48),
|
|
165
|
+
stagingHost: "staging.example.com",
|
|
166
|
+
stagingGateSecret: "g".repeat(64),
|
|
165
167
|
diskSize: "20Gi",
|
|
166
168
|
images: {
|
|
167
169
|
mode: "release" as const,
|
package/src/gcp-names.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the CLI's dependencies.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
18
20
|
/** The Pulumi stack name the wizard provisions and `upgrade` operates on. */
|
|
19
21
|
export const GCP_STACK_ENV = "production";
|
|
20
22
|
|
|
@@ -87,6 +89,57 @@ export function mcpAuthProxyServiceAccountEmail(projectId: string, env: string):
|
|
|
87
89
|
return `${mcpAuthProxyServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* #1330 — account id of the service account the staging gateway runs as. It
|
|
94
|
+
* may read the staging database's login password and the staging bucket,
|
|
95
|
+
* and nothing else: no live data, no media, no gate key (the gate key
|
|
96
|
+
* reaches it through the staging projection). Shortened: account ids max
|
|
97
|
+
* out at 30 characters (`caelo-production-stg-gateway-sa` would be 31).
|
|
98
|
+
*/
|
|
99
|
+
export function stagingGatewayServiceAccountId(env: string): string {
|
|
100
|
+
return `${gcpNamePrefix(env)}-stg-gw-sa`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Email of the service account the staging gateway runs as. */
|
|
104
|
+
export function stagingGatewayServiceAccountEmail(projectId: string, env: string): string {
|
|
105
|
+
return `${stagingGatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* #1330 — the `gcp` staging gateway's load-balancer resources, CLI-owned
|
|
110
|
+
* (staging-gateway-setup.ts): its serverless NEG, its backend service and
|
|
111
|
+
* the managed certificate for the staging host. None matches a prefix the
|
|
112
|
+
* other lookups use (`…-waf`, `…-admin-backend`, `…-gateway-backend`,
|
|
113
|
+
* `…-mcp-auth-proxy-*`).
|
|
114
|
+
*/
|
|
115
|
+
export function stagingGatewayEdgeNames(env: string): {
|
|
116
|
+
readonly neg: string;
|
|
117
|
+
readonly backend: string;
|
|
118
|
+
readonly cert: string;
|
|
119
|
+
readonly pathMatcher: string;
|
|
120
|
+
} {
|
|
121
|
+
const prefix = `${gcpNamePrefix(env)}-staging-gateway`;
|
|
122
|
+
return {
|
|
123
|
+
neg: `${prefix}-neg`,
|
|
124
|
+
backend: `${prefix}-backend`,
|
|
125
|
+
cert: `${gcpNamePrefix(env)}-staging-cert`,
|
|
126
|
+
pathMatcher: "staging",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* #1330 — the second Firebase Hosting site of a `gcp-firebase` install,
|
|
132
|
+
* which serves the staging host through a `**` rewrite to the staging
|
|
133
|
+
* gateway (E13 a of #763). Derived from the live site's id (Hosting site ids
|
|
134
|
+
* are global, and the live one carries a random suffix): `caelo-<env>-stg-`
|
|
135
|
+
* plus six hex characters of its SHA-256, so the admin and the converge,
|
|
136
|
+
* which both know the live site id (`CAELO_FIREBASE_SITE`), name the same
|
|
137
|
+
* site without storing it anywhere.
|
|
138
|
+
*/
|
|
139
|
+
export function firebaseStagingSiteId(env: string, liveSiteId: string): string {
|
|
140
|
+
return `${gcpNamePrefix(env)}-stg-${createHash("sha256").update(liveSiteId).digest("hex").slice(0, 6)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
90
143
|
/**
|
|
91
144
|
* Account id of the `gcp` stack's static-publisher service account. Account
|
|
92
145
|
* ids max out at 30 chars, so the env is shortened.
|
package/src/gcp-url-map.test.ts
CHANGED
|
@@ -46,6 +46,7 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
46
46
|
{
|
|
47
47
|
domain: "example.com",
|
|
48
48
|
adminDomain: "admin.example.com",
|
|
49
|
+
stagingHost: "staging.example.com",
|
|
49
50
|
staticBackend: "bucket-id",
|
|
50
51
|
gatewayBackend: "gateway-id",
|
|
51
52
|
adminBackend: "admin-id",
|
|
@@ -61,6 +62,8 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
61
62
|
expect(hostRules).toEqual([
|
|
62
63
|
{ hosts: ["example.com"], pathMatcher: PUBLIC_PATH_MATCHER },
|
|
63
64
|
{ hosts: ["admin.example.com"], pathMatcher: "admin" },
|
|
65
|
+
// #1330 — the staging host, its matcher moved to the staging gateway by the CLI.
|
|
66
|
+
{ hosts: ["staging.example.com"], pathMatcher: "staging" },
|
|
64
67
|
]);
|
|
65
68
|
expect(pathMatchers).toEqual([
|
|
66
69
|
{
|
|
@@ -70,7 +73,20 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
70
73
|
pathRules: [{ paths: [...GCP_GATEWAY_PATHS], service: "gateway-id" }],
|
|
71
74
|
},
|
|
72
75
|
{ name: "admin", defaultService: "admin-id" },
|
|
76
|
+
// #1330 — fails closed until the CLI routes it to the staging gateway:
|
|
77
|
+
// a redirect, never a backend (above all not the static bucket).
|
|
78
|
+
{
|
|
79
|
+
name: "staging",
|
|
80
|
+
defaultUrlRedirect: {
|
|
81
|
+
hostRedirect: "example.com",
|
|
82
|
+
pathRedirect: "/",
|
|
83
|
+
httpsRedirect: true,
|
|
84
|
+
redirectResponseCode: "FOUND",
|
|
85
|
+
stripQuery: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
73
88
|
]);
|
|
89
|
+
expect(JSON.stringify(pathMatchers?.[2])).not.toContain("bucket-id");
|
|
74
90
|
expect(registered.find((r) => r.name === "caelo-production-url-map")?.type).toBe(
|
|
75
91
|
"gcp:compute/uRLMap:URLMap",
|
|
76
92
|
);
|
|
@@ -115,16 +131,24 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
|
|
|
115
131
|
const { SITE_URL_MAP_IGNORE_CHANGES, siteUrlMapArgs, PUBLIC_PATH_MATCHER } = await import(
|
|
116
132
|
"./gcp-url-map.js"
|
|
117
133
|
);
|
|
118
|
-
expect(SITE_URL_MAP_IGNORE_CHANGES).toEqual([
|
|
134
|
+
expect(SITE_URL_MAP_IGNORE_CHANGES).toEqual([
|
|
135
|
+
"pathMatchers[0].pathRules",
|
|
136
|
+
// #1330 — the staging matcher's target is the CLI's.
|
|
137
|
+
"pathMatchers[2].defaultService",
|
|
138
|
+
"pathMatchers[2].defaultUrlRedirect",
|
|
139
|
+
]);
|
|
119
140
|
const args = siteUrlMapArgs({
|
|
120
141
|
domain: "d",
|
|
121
142
|
adminDomain: "a.d",
|
|
143
|
+
stagingHost: "s.d",
|
|
122
144
|
staticBackend: "s",
|
|
123
145
|
gatewayBackend: "g",
|
|
124
146
|
adminBackend: "a",
|
|
125
147
|
});
|
|
126
|
-
// Index 0 must stay the public matcher, or the ignore would hit the admin's
|
|
148
|
+
// Index 0 must stay the public matcher, or the ignore would hit the admin's;
|
|
149
|
+
// index 2 the staging one (the CLI writes it last, staging-gateway-setup.ts).
|
|
127
150
|
expect((args.pathMatchers as { name: string }[])[0]?.name).toBe(PUBLIC_PATH_MATCHER);
|
|
151
|
+
expect((args.pathMatchers as { name: string }[])[2]?.name).toBe("staging");
|
|
128
152
|
});
|
|
129
153
|
});
|
|
130
154
|
|
package/src/gcp-url-map.ts
CHANGED
|
@@ -26,16 +26,28 @@ import type * as pulumi from "@pulumi/pulumi";
|
|
|
26
26
|
/** Name of the site host's path matcher. */
|
|
27
27
|
export const PUBLIC_PATH_MATCHER = "public";
|
|
28
28
|
|
|
29
|
+
/** #1330 — name of the staging host's path matcher (gcp-names.ts stagingGatewayEdgeNames). */
|
|
30
|
+
export const STAGING_PATH_MATCHER = "staging";
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* The URL map's `ignoreChanges`: the public path matcher (index 0, see
|
|
31
|
-
* {@link siteUrlMapArgs}) holds the admin-managed redirect rules
|
|
34
|
+
* {@link siteUrlMapArgs}) holds the admin-managed redirect rules, and the
|
|
35
|
+
* staging matcher's (index 2) target is the CLI's: the stack creates it as a
|
|
36
|
+
* redirect, and staging-gateway-setup.ts ensureStagingGatewayEdge (#1330)
|
|
37
|
+
* replaces that with the staging gateway's backend once it exists.
|
|
32
38
|
*/
|
|
33
|
-
export const SITE_URL_MAP_IGNORE_CHANGES: readonly string[] = [
|
|
39
|
+
export const SITE_URL_MAP_IGNORE_CHANGES: readonly string[] = [
|
|
40
|
+
"pathMatchers[0].pathRules",
|
|
41
|
+
"pathMatchers[2].defaultService",
|
|
42
|
+
"pathMatchers[2].defaultUrlRedirect",
|
|
43
|
+
];
|
|
34
44
|
|
|
35
45
|
/** Inputs of {@link siteUrlMapArgs}. */
|
|
36
46
|
export interface SiteUrlMapInputs {
|
|
37
47
|
readonly domain: string;
|
|
38
48
|
readonly adminDomain: string;
|
|
49
|
+
/** #1330 — the protected staging site's host. */
|
|
50
|
+
readonly stagingHost: string;
|
|
39
51
|
readonly staticBackend: pulumi.Input<string>;
|
|
40
52
|
readonly gatewayBackend: pulumi.Input<string>;
|
|
41
53
|
readonly adminBackend: pulumi.Input<string>;
|
|
@@ -43,9 +55,17 @@ export interface SiteUrlMapInputs {
|
|
|
43
55
|
|
|
44
56
|
/**
|
|
45
57
|
* The URL map's arguments: the site host to the static bucket with `/api/*`
|
|
46
|
-
* to the gateway; the admin host to the IAP-gated admin
|
|
47
|
-
*
|
|
48
|
-
*
|
|
58
|
+
* to the gateway; the admin host to the IAP-gated admin; (#1330) the staging
|
|
59
|
+
* host, every path, to the staging gateway. The public path matcher comes
|
|
60
|
+
* first and the staging one third — {@link SITE_URL_MAP_IGNORE_CHANGES}
|
|
61
|
+
* addresses them by index.
|
|
62
|
+
*
|
|
63
|
+
* The staging matcher fails closed until the staging gateway's backend
|
|
64
|
+
* exists (it is the CLI's, created after the first `up`): it serves nothing
|
|
65
|
+
* at all, it redirects every staging request to the live site's home page.
|
|
66
|
+
* Never a backend — above all never the static bucket — so a failed edge
|
|
67
|
+
* step can never show a file on the staging host. The CLI then routes it to
|
|
68
|
+
* the staging gateway, and the stack leaves it there.
|
|
49
69
|
*/
|
|
50
70
|
export function siteUrlMapArgs(i: SiteUrlMapInputs): gcp.compute.URLMapArgs {
|
|
51
71
|
return {
|
|
@@ -53,6 +73,7 @@ export function siteUrlMapArgs(i: SiteUrlMapInputs): gcp.compute.URLMapArgs {
|
|
|
53
73
|
hostRules: [
|
|
54
74
|
{ hosts: [i.domain], pathMatcher: PUBLIC_PATH_MATCHER },
|
|
55
75
|
{ hosts: [i.adminDomain], pathMatcher: "admin" },
|
|
76
|
+
{ hosts: [i.stagingHost], pathMatcher: STAGING_PATH_MATCHER },
|
|
56
77
|
],
|
|
57
78
|
pathMatchers: [
|
|
58
79
|
{
|
|
@@ -68,6 +89,16 @@ export function siteUrlMapArgs(i: SiteUrlMapInputs): gcp.compute.URLMapArgs {
|
|
|
68
89
|
name: "admin",
|
|
69
90
|
defaultService: i.adminBackend,
|
|
70
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: STAGING_PATH_MATCHER,
|
|
94
|
+
defaultUrlRedirect: {
|
|
95
|
+
hostRedirect: i.domain,
|
|
96
|
+
pathRedirect: "/",
|
|
97
|
+
httpsRedirect: true,
|
|
98
|
+
redirectResponseCode: "FOUND",
|
|
99
|
+
stripQuery: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
71
102
|
],
|
|
72
103
|
};
|
|
73
104
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ describe("Caddyfile generator", () => {
|
|
|
18
18
|
const out = generateCaddyfile({
|
|
19
19
|
ownerEmail: "owner@example.com",
|
|
20
20
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
21
|
-
|
|
21
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
22
22
|
tls: "acme",
|
|
23
23
|
adminUpstream: "caelo-admin:5173",
|
|
24
24
|
gatewayUpstream: "caelo-gateway:8090",
|
|
@@ -46,7 +46,7 @@ describe("Caddyfile generator", () => {
|
|
|
46
46
|
const out = generateCaddyfile({
|
|
47
47
|
ownerEmail: "x@y.z",
|
|
48
48
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
49
|
-
|
|
49
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
50
50
|
tls: "acme",
|
|
51
51
|
adminUpstream: "caelo-admin:5173",
|
|
52
52
|
gatewayUpstream: "caelo-gateway:8090",
|
|
@@ -62,7 +62,7 @@ describe("Caddy cache policy for content-hashed assets", () => {
|
|
|
62
62
|
const spec = {
|
|
63
63
|
ownerEmail: "owner@example.com",
|
|
64
64
|
publicSiteRoot: "/srv/caelo/output/production/current",
|
|
65
|
-
|
|
65
|
+
stagingGatewayUpstream: "caelo-staging-gateway:8091",
|
|
66
66
|
tls: "acme",
|
|
67
67
|
adminUpstream: "caelo-admin:5173",
|
|
68
68
|
gatewayUpstream: "caelo-gateway:8090",
|
|
@@ -156,6 +156,8 @@ describe("docker-compose generator", () => {
|
|
|
156
156
|
caeloSecretKek: "0".repeat(64),
|
|
157
157
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
158
158
|
internalSecret: "i".repeat(48),
|
|
159
|
+
stagingHost: "staging.example.com",
|
|
160
|
+
stagingGateSecret: "g".repeat(64),
|
|
159
161
|
anthropicApiKey: "sk-ant-…",
|
|
160
162
|
diskSize: "20Gi",
|
|
161
163
|
images: RELEASE_IMAGES,
|
|
@@ -179,6 +181,8 @@ describe("docker-compose generator", () => {
|
|
|
179
181
|
caeloSecretKek: "0".repeat(64),
|
|
180
182
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
181
183
|
internalSecret: "i".repeat(48),
|
|
184
|
+
stagingHost: "staging.example.com",
|
|
185
|
+
stagingGateSecret: "g".repeat(64),
|
|
182
186
|
diskSize: "20Gi",
|
|
183
187
|
images: RELEASE_IMAGES,
|
|
184
188
|
ports: { http: 80, https: 443 },
|
|
@@ -205,6 +209,8 @@ describe("docker-compose generator", () => {
|
|
|
205
209
|
caeloSecretKek: "0".repeat(64),
|
|
206
210
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
207
211
|
internalSecret: "i".repeat(48),
|
|
212
|
+
stagingHost: "staging.example.com",
|
|
213
|
+
stagingGateSecret: "g".repeat(64),
|
|
208
214
|
diskSize: "1Gi",
|
|
209
215
|
images: RELEASE_IMAGES,
|
|
210
216
|
ports: { http: 80, https: 443 },
|
|
@@ -20,8 +20,8 @@ describe("infrastructure contract steps", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("is at
|
|
24
|
-
expect(INFRA_CONTRACT_VERSION).toBe(
|
|
23
|
+
it("is at 20: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13), capture job (14), database verify-full TLS (15), MCP-Auth-Proxy (16), staging database (17), scheduler tick (18), scheduler tick as JSON (19), staging gateway (20)", () => {
|
|
24
|
+
expect(INFRA_CONTRACT_VERSION).toBe(20);
|
|
25
25
|
expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
|
|
26
26
|
expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
|
|
27
27
|
expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
|
|
@@ -38,6 +38,8 @@ describe("infrastructure contract steps", () => {
|
|
|
38
38
|
expect(INFRA_CONTRACT_STEPS[16]).toContain("#1328");
|
|
39
39
|
expect(INFRA_CONTRACT_STEPS[17]).toContain("#1329");
|
|
40
40
|
expect(INFRA_CONTRACT_STEPS[18]).toContain("#1140");
|
|
41
|
+
expect(INFRA_CONTRACT_STEPS[19]).toContain("JSON");
|
|
42
|
+
expect(INFRA_CONTRACT_STEPS[20]).toContain("#1330");
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it("lists the pending steps oldest first", () => {
|
|
@@ -48,6 +50,6 @@ describe("infrastructure contract steps", () => {
|
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
it("names a version a newer release added by number", () => {
|
|
51
|
-
expect(infraStepsText(
|
|
53
|
+
expect(infraStepsText(20, 21)).toBe("infrastructure step 21 (see that release's notes)");
|
|
52
54
|
});
|
|
53
55
|
});
|
package/src/infra-contract.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* whose app-roll references infrastructure an install does not have yet fails
|
|
23
23
|
* at roll time, after migrations ran.
|
|
24
24
|
*/
|
|
25
|
-
export const INFRA_CONTRACT_VERSION =
|
|
25
|
+
export const INFRA_CONTRACT_VERSION = 20;
|
|
26
26
|
|
|
27
27
|
// History (why each bump happened — an install below a number needs the
|
|
28
28
|
// converge that introduced it):
|
|
@@ -150,6 +150,25 @@ export const INFRA_CONTRACT_VERSION = 18;
|
|
|
150
150
|
// on the admin. The new admin runs its periodic work and plugin workers
|
|
151
151
|
// only on that tick, so an in-cloud upgrader must not roll an install
|
|
152
152
|
// onto it before the tick exists.
|
|
153
|
+
// 19 — the scheduler workflow's tick is a JSON POST. Without a content type
|
|
154
|
+
// SvelteKit refused every tick of contract 18 with 403, so no periodic
|
|
155
|
+
// work ran. The workflow source is only deployed by a converge.
|
|
156
|
+
// 20 — #1330/#1331: the staging gateway (converge release 2 of the gateway
|
|
157
|
+
// hardening). Its service account (secretAccessor on
|
|
158
|
+
// public-staging-role-password only, log/metric writer), its Cloud Run
|
|
159
|
+
// service `caelo-<env>-staging-gateway` (the gateway's image,
|
|
160
|
+
// CAELO_GATEWAY_ENVIRONMENT=staging, allUsers invoker: the gate is its
|
|
161
|
+
// own), object viewer on the staging bucket — on gcp-firebase a new
|
|
162
|
+
// private bucket, which the admin may write (CAELO_STAGING_BUCKET) —
|
|
163
|
+
// and its edge: on gcp-firebase a second Hosting site with one `**`
|
|
164
|
+
// rewrite and the staging host as its custom domain, the old Cloud Run
|
|
165
|
+
// domain mapping deleted; on gcp a serverless NEG, backend service,
|
|
166
|
+
// managed certificate and URL-map host rule. The live gateway loses
|
|
167
|
+
// CAELO_STAGING_GATE_SECRET (the gate key reaches the staging gateway
|
|
168
|
+
// through the staging projection) and the MCP-Auth-Proxy its interim
|
|
169
|
+
// forwarder. New service, account, bucket, IAM and edge resources the
|
|
170
|
+
// in-cloud upgrader may not create, and the app-roll rolls four
|
|
171
|
+
// services from now on.
|
|
153
172
|
|
|
154
173
|
/**
|
|
155
174
|
* What each contract version adds, in operator terms — the upgrader's
|
|
@@ -175,6 +194,8 @@ export const INFRA_CONTRACT_STEPS: Readonly<Record<number, string>> = {
|
|
|
175
194
|
16: "the MCP sign-in service as its own Cloud Run service with its own account (no database access), the visitor gateway as its own service on Firebase installs, and on Google Cloud load-balancer installs the sign-in service's backend and Cloud Armor rule (#1328)",
|
|
176
195
|
17: "the staging site's own database and its gateway login, kept apart from the live data (#1329)",
|
|
177
196
|
18: "the scheduler tick that runs the admin's periodic work and plugin workers every few minutes: its service account, workflow and Cloud Scheduler job, and its IAP access (#1140)",
|
|
197
|
+
19: "the scheduler tick sent as a JSON request, because the admin refused every tick before (#1140)",
|
|
198
|
+
20: "the staging gateway that serves the staged site behind the staging gate on every address: its service, service account and grants, the private staging bucket, and the staging host's own route (a second Firebase Hosting site, or a host rule and certificate on the load balancer); the live gateway no longer holds the staging gate key (#1330, #1331)",
|
|
178
199
|
};
|
|
179
200
|
|
|
180
201
|
/**
|