@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/upgrader-job.ts
CHANGED
|
@@ -63,14 +63,17 @@ import {
|
|
|
63
63
|
type ReleaseManifest,
|
|
64
64
|
type ReleaseManifestResult,
|
|
65
65
|
} from "./release-manifest.js";
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
cloudRunServicePrefix,
|
|
68
|
+
SERVICE_IMAGE_NAMES,
|
|
69
|
+
type ServiceImageName,
|
|
70
|
+
} from "./stack-contract.js";
|
|
67
71
|
import { StateLockHeldError } from "./state-store.js";
|
|
68
72
|
import {
|
|
69
73
|
type AppRollPlan,
|
|
70
74
|
executeAppRoll,
|
|
71
75
|
planAppRoll,
|
|
72
76
|
type ReleaseImage,
|
|
73
|
-
ROLL_ORDER,
|
|
74
77
|
type RollSlug,
|
|
75
78
|
} from "./upgrade/app-roll.js";
|
|
76
79
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
@@ -699,7 +702,7 @@ export async function recordUpdateEnd(
|
|
|
699
702
|
return false;
|
|
700
703
|
}
|
|
701
704
|
|
|
702
|
-
/** Every digest the revisions serving traffic are known by, per
|
|
705
|
+
/** Every digest the revisions serving traffic are known by, per release image. */
|
|
703
706
|
async function servingDigests(
|
|
704
707
|
deps: UpgraderJobDeps,
|
|
705
708
|
provider: InstallMetadata["provider"],
|
|
@@ -708,16 +711,18 @@ async function servingDigests(
|
|
|
708
711
|
ok: true;
|
|
709
712
|
digests: ServingDigests;
|
|
710
713
|
/** The services' deployed names, for the template hints. */
|
|
711
|
-
services: Readonly<Record<
|
|
714
|
+
services: Readonly<Record<ServiceImageName, string>>;
|
|
712
715
|
}
|
|
713
716
|
| { ok: false; error: string }
|
|
714
717
|
> {
|
|
715
|
-
const digests: Partial<Record<
|
|
716
|
-
const services: Partial<Record<
|
|
718
|
+
const digests: Partial<Record<ServiceImageName, readonly string[]>> = {};
|
|
719
|
+
const services: Partial<Record<ServiceImageName, string>> = {};
|
|
717
720
|
if (provider !== "gcp" && provider !== "gcp-firebase") {
|
|
718
721
|
return { ok: false, error: `the upgrader runs on Google Cloud installs only, not ${provider}` };
|
|
719
722
|
}
|
|
720
|
-
|
|
723
|
+
// The staging gateway (#1330) runs the gateway's image and rolls with
|
|
724
|
+
// it; the images, one service each, name the release.
|
|
725
|
+
for (const slug of SERVICE_IMAGE_NAMES) {
|
|
721
726
|
// #1328 — which service plays which role differs per provider.
|
|
722
727
|
const prefix = cloudRunServicePrefix(provider, deps.env, slug);
|
|
723
728
|
const service = await resolveGcpResourceName(
|
|
@@ -735,10 +740,39 @@ async function servingDigests(
|
|
|
735
740
|
digests[slug] = servingImageDigests(image);
|
|
736
741
|
services[slug] = service.name;
|
|
737
742
|
}
|
|
743
|
+
// #1330 — the staging gateway must serve the very image the gateway
|
|
744
|
+
// serves: it rolls with it, and an install whose staging gateway runs
|
|
745
|
+
// another release is not on one release (the upgrader runs only on
|
|
746
|
+
// installs converged to contract 20, which have it).
|
|
747
|
+
const stagingPrefix = cloudRunServicePrefix(provider, deps.env, "staging-gateway");
|
|
748
|
+
const staging = await resolveGcpResourceName(
|
|
749
|
+
deps.run,
|
|
750
|
+
"run-service",
|
|
751
|
+
stagingPrefix,
|
|
752
|
+
deps.projectId,
|
|
753
|
+
deps.region,
|
|
754
|
+
);
|
|
755
|
+
if (staging.status !== "found") {
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
error: lookupFailure(staging, `${stagingPrefix}* Cloud Run service`, "job"),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const stagingImage = await findServingImage(deps.run, deps.projectId, deps.region, staging.name);
|
|
762
|
+
if (!stagingImage) {
|
|
763
|
+
return { ok: false, error: "could not read which image the staging gateway serves" };
|
|
764
|
+
}
|
|
765
|
+
const stagingDigests = servingImageDigests(stagingImage);
|
|
766
|
+
if (!stagingDigests.some((d) => (digests.gateway ?? []).includes(d))) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
error: `the staging gateway (${staging.name}) serves ${stagingDigests[0] ?? "an unknown image"}, but the gateway serves ${digests.gateway?.[0] ?? "an unknown image"}: the install does not run one release. Nothing was changed; an upgrade from Cloud Shell rolls both onto one release.`,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
738
772
|
return {
|
|
739
773
|
ok: true,
|
|
740
774
|
digests: digests as ServingDigests,
|
|
741
|
-
services: services as Record<
|
|
775
|
+
services: services as Record<ServiceImageName, string>,
|
|
742
776
|
};
|
|
743
777
|
}
|
|
744
778
|
|
|
@@ -749,10 +783,10 @@ async function servingDigests(
|
|
|
749
783
|
*/
|
|
750
784
|
async function templateDigests(
|
|
751
785
|
deps: UpgraderJobDeps,
|
|
752
|
-
services: Readonly<Record<
|
|
786
|
+
services: Readonly<Record<ServiceImageName, string>>,
|
|
753
787
|
): Promise<ServingDigests> {
|
|
754
|
-
const out: Partial<Record<
|
|
755
|
-
for (const slug of
|
|
788
|
+
const out: Partial<Record<ServiceImageName, readonly string[]>> = {};
|
|
789
|
+
for (const slug of SERVICE_IMAGE_NAMES) {
|
|
756
790
|
const d = await findTemplateImageDigest(deps.run, deps.projectId, deps.region, services[slug]);
|
|
757
791
|
out[slug] = d ? [d] : [];
|
|
758
792
|
}
|
package/src/upgrader-records.ts
CHANGED
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
31
31
|
import type { ImageDigests, InstallMetadata } from "./install-state.js";
|
|
32
32
|
import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
|
|
33
|
-
import {
|
|
33
|
+
import { SERVICE_IMAGE_NAMES, type ServiceImageName } from "./stack-contract.js";
|
|
34
34
|
import type { ReleaseImageForms } from "./upgrade/image-forms.js";
|
|
35
35
|
import { releaseVersionForDigest } from "./upgrade/release-images.js";
|
|
36
36
|
|
|
37
37
|
/** Every digest each serving image is known by (gcp-lookup.ts servingImageDigests). */
|
|
38
|
-
export type ServingDigests = Readonly<Record<
|
|
38
|
+
export type ServingDigests = Readonly<Record<ServiceImageName, readonly string[]>>;
|
|
39
39
|
|
|
40
40
|
// #1328 — the MCP-Auth-Proxy serves on every install this code runs on (the
|
|
41
41
|
// upgrader runs only on installs converged to its contract).
|
|
42
|
-
const SLUGS: readonly
|
|
42
|
+
const SLUGS: readonly ServiceImageName[] = SERVICE_IMAGE_NAMES;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Whether install.json names the images that serve: per service, the
|
|
@@ -176,7 +176,7 @@ export async function findServingRelease(
|
|
|
176
176
|
type ReleaseJudgement =
|
|
177
177
|
| {
|
|
178
178
|
readonly kind: "judged";
|
|
179
|
-
readonly matching: readonly
|
|
179
|
+
readonly matching: readonly ServiceImageName[];
|
|
180
180
|
readonly platformDigests: ImageDigests | null;
|
|
181
181
|
}
|
|
182
182
|
| { readonly kind: "unreadable"; readonly transient: boolean; readonly message: string };
|
|
@@ -191,8 +191,8 @@ async function judgeRelease(
|
|
|
191
191
|
serving: ServingDigests,
|
|
192
192
|
imageForms: ReleaseImageForms,
|
|
193
193
|
): Promise<ReleaseJudgement> {
|
|
194
|
-
const matching:
|
|
195
|
-
const platform: Partial<Record<
|
|
194
|
+
const matching: ServiceImageName[] = [];
|
|
195
|
+
const platform: Partial<Record<ServiceImageName, string>> = {};
|
|
196
196
|
for (const slug of SLUGS) {
|
|
197
197
|
const index = m.digests[slug];
|
|
198
198
|
// #1328 — a release from before the proxy cannot be what serves here.
|
|
@@ -257,7 +257,7 @@ function capitalize(s: string): string {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/** `the admin`, `the gateway and the MCP-Auth-Proxy`, … for the refusal text. */
|
|
260
|
-
function serviceList(slugs: readonly
|
|
260
|
+
function serviceList(slugs: readonly ServiceImageName[]): string {
|
|
261
261
|
const names = slugs.map((s) => (s === "mcp-auth-proxy" ? "the MCP-Auth-Proxy" : `the ${s}`));
|
|
262
262
|
if (names.length <= 1) return names[0] ?? "";
|
|
263
263
|
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
package/src/wizard.ts
CHANGED
|
@@ -154,7 +154,7 @@ export async function runWizard(opts: WizardOptions = {}): Promise<void> {
|
|
|
154
154
|
} else {
|
|
155
155
|
provider = jobModeProvider ?? opts.provider ?? (await pickProvider());
|
|
156
156
|
domain = opts.domain ?? (await promptDomain());
|
|
157
|
-
const stagingDomain = await resolveStagingDomain(domain,
|
|
157
|
+
const stagingDomain = await resolveStagingDomain(domain, opts);
|
|
158
158
|
ownerEmail = opts.ownerEmail ?? (await promptOwnerEmail());
|
|
159
159
|
projectIdHint = jobModeProjectId ?? opts.projectId ?? null;
|
|
160
160
|
installId = deriveInstallId(provider, projectIdHint ?? domain);
|
|
@@ -389,11 +389,10 @@ async function promptDomain(): Promise<string> {
|
|
|
389
389
|
*/
|
|
390
390
|
async function resolveStagingDomain(
|
|
391
391
|
domain: string,
|
|
392
|
-
provider: Provider,
|
|
393
392
|
opts: Pick<WizardOptions, "stagingDomain" | "nonInteractive">,
|
|
394
393
|
): Promise<string> {
|
|
395
394
|
const reserved = { liveHosts: [domain, `www.${domain}`], adminHosts: [`admin.${domain}`] };
|
|
396
|
-
const problemOf = (host: string) => stagingHostProblem(host, reserved
|
|
395
|
+
const problemOf = (host: string) => stagingHostProblem(host, reserved);
|
|
397
396
|
if (opts.stagingDomain) {
|
|
398
397
|
const problem = problemOf(opts.stagingDomain);
|
|
399
398
|
if (problem) {
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -129,6 +129,10 @@ import {
|
|
|
129
129
|
type RemoteMcpIdentity,
|
|
130
130
|
sameRemoteMcpIdentity,
|
|
131
131
|
} from "../stack-contract.js";
|
|
132
|
+
import {
|
|
133
|
+
ensureStagingGatewayEdge,
|
|
134
|
+
ensureStagingGatewayFromLiveGateway,
|
|
135
|
+
} from "../staging-gateway-setup.js";
|
|
132
136
|
import {
|
|
133
137
|
chooseInstallBackend,
|
|
134
138
|
describeBackend,
|
|
@@ -521,6 +525,18 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
521
525
|
|
|
522
526
|
phase("Finishing: operator access, welcome page");
|
|
523
527
|
|
|
528
|
+
// === 11.2. The staging gateway and its edge (#1330) ===
|
|
529
|
+
// Before the audit job, which reads the staging bucket this step creates
|
|
530
|
+
// on gcp-firebase.
|
|
531
|
+
const stagingReady = await stepStagingGateway(
|
|
532
|
+
projectId,
|
|
533
|
+
region,
|
|
534
|
+
provider,
|
|
535
|
+
domain,
|
|
536
|
+
installStagingHost(meta),
|
|
537
|
+
imageDigests,
|
|
538
|
+
);
|
|
539
|
+
|
|
524
540
|
// === 11.3. The quality-audit job: parallel Lighthouse runs (#875) ===
|
|
525
541
|
// Before the upgrader, which gets actAs on the job's service account.
|
|
526
542
|
await stepAuditJob(projectId, region, provider);
|
|
@@ -573,10 +589,10 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
573
589
|
// A fresh install ran every converge step above, so it is at this CLI's
|
|
574
590
|
// infrastructure contract — recorded only when the upgrader and the
|
|
575
591
|
// converge job are in place (the steps contract bumps 2 and 9 added) and
|
|
576
|
-
// remote MCP's steps (#806, bump 10)
|
|
577
|
-
//
|
|
592
|
+
// remote MCP's steps (#806, bump 10), the scheduler tick (#1140, bump 18)
|
|
593
|
+
// and the staging gateway (#1330, bump 20) succeeded. With the release version, the admin can install the next
|
|
578
594
|
// update itself.
|
|
579
|
-
if (upgraderReady && convergeReady && remoteMcpReady && schedulerReady) {
|
|
595
|
+
if (upgraderReady && convergeReady && remoteMcpReady && schedulerReady && stagingReady) {
|
|
580
596
|
const version = releaseVersion;
|
|
581
597
|
// #1025 — the linux/amd64 forms Cloud Run reports, next to the indexes,
|
|
582
598
|
// so the upgrader recognises what serves without a registry lookup.
|
|
@@ -1465,6 +1481,49 @@ async function stepRemoteMcp(
|
|
|
1465
1481
|
return true;
|
|
1466
1482
|
}
|
|
1467
1483
|
|
|
1484
|
+
/**
|
|
1485
|
+
* #1330 — the staging gateway (the live gateway's image, gate on, serving the
|
|
1486
|
+
* staged build from the staging bucket) and the route of the staging host to
|
|
1487
|
+
* it (staging-gateway-setup.ts). The site works without it, so a failure
|
|
1488
|
+
* warns and `upgrade` retries. Prints the DNS records the staging host needs.
|
|
1489
|
+
*
|
|
1490
|
+
* @returns whether both parts are in place
|
|
1491
|
+
*/
|
|
1492
|
+
async function stepStagingGateway(
|
|
1493
|
+
projectId: string,
|
|
1494
|
+
region: string,
|
|
1495
|
+
provider: "gcp" | "gcp-firebase",
|
|
1496
|
+
domain: string,
|
|
1497
|
+
stagingHost: string,
|
|
1498
|
+
imageDigests: ImageDigests,
|
|
1499
|
+
): Promise<boolean> {
|
|
1500
|
+
const s = spinner();
|
|
1501
|
+
s.start(`Setting up the staging gateway behind the staging gate (${stagingHost})...`);
|
|
1502
|
+
const failed = (error: string) => {
|
|
1503
|
+
s.stop(yellow(`Staging gateway not set up: ${error}`));
|
|
1504
|
+
log.warn(
|
|
1505
|
+
`The site works; ${stagingHost} answers nothing until ${bold(selfCommand("upgrade"))} sets it up.`,
|
|
1506
|
+
);
|
|
1507
|
+
return false;
|
|
1508
|
+
};
|
|
1509
|
+
const target = { provider, projectId, region, env: GCP_STACK_ENV, domain, stagingHost };
|
|
1510
|
+
const service = await ensureStagingGatewayFromLiveGateway({
|
|
1511
|
+
...target,
|
|
1512
|
+
gatewayImageRef: releaseImageRef("gateway", imageDigests.gateway),
|
|
1513
|
+
});
|
|
1514
|
+
const broken = service.find((o) => o.status === "failed");
|
|
1515
|
+
if (broken) return failed(`${broken.id}: ${broken.error ?? ""}`);
|
|
1516
|
+
const edge = await ensureStagingGatewayEdge(target);
|
|
1517
|
+
if (!edge.ok) return failed(edge.error);
|
|
1518
|
+
s.stop(green(`Staging gateway ready: ${stagingHost} is served behind the staging gate`));
|
|
1519
|
+
for (const line of edge.done) log.info(` ${line}`);
|
|
1520
|
+
if (edge.dnsRecords.length > 0) {
|
|
1521
|
+
log.info(` DNS for ${stagingHost} (Settings → Updates shows the same):`);
|
|
1522
|
+
for (const d of edge.dnsRecords) log.info(` ${d.name} ${d.type} ${d.value}`);
|
|
1523
|
+
}
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1468
1527
|
/**
|
|
1469
1528
|
* Delete the firewall rules GCP pre-populates on the `default` network
|
|
1470
1529
|
* (SSH, RDP, ICMP, internal; default-firewall.ts). A customised rule of the
|
|
@@ -2436,14 +2495,19 @@ async function stepFinalize(installId: string, setupLink: "print" | "secret"): P
|
|
|
2436
2495
|
` To bind ${cyan(`admin.${meta.domain}`)}, run`,
|
|
2437
2496
|
` ${dim(selfCommand("admin-domain", "enable"))}`,
|
|
2438
2497
|
` (it walks you through the one-time Search Console verification, prints the DNS record and waits for the certificate).`,
|
|
2439
|
-
"",
|
|
2440
|
-
bold("Protected staging site:"),
|
|
2441
|
-
` Stage builds render ${cyan(installStagingHost(meta))}; to serve it, run`,
|
|
2442
|
-
` ${dim(selfCommand("staging-domain", "enable"))}`,
|
|
2443
|
-
` It maps the host, turns on password protection and prints the staging login once;`,
|
|
2444
|
-
` add the DNS record it prints. The host serves pages after the first Stage.`,
|
|
2445
2498
|
]
|
|
2446
2499
|
: [];
|
|
2500
|
+
// #1330 — staging is served by its own gateway on every GCP install; the
|
|
2501
|
+
// records it needs were printed by the staging gateway step above.
|
|
2502
|
+
adminNote.push(
|
|
2503
|
+
"",
|
|
2504
|
+
bold("Protected staging site:"),
|
|
2505
|
+
` ${cyan(installStagingHost(meta))} is served by the staging gateway behind the staging gate,`,
|
|
2506
|
+
" once the DNS records printed above (also under Settings → Updates) point at it.",
|
|
2507
|
+
` To turn on password protection and print the staging login once, run`,
|
|
2508
|
+
` ${dim(selfCommand("staging-domain", "enable"))}`,
|
|
2509
|
+
" The host serves pages after the first Stage.",
|
|
2510
|
+
);
|
|
2447
2511
|
|
|
2448
2512
|
note(
|
|
2449
2513
|
[
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
MCP_IAP_SERVICE_ACCOUNT_ID,
|
|
30
30
|
mcpAuthProxyServiceAccountId,
|
|
31
31
|
runServiceAccountId,
|
|
32
|
+
stagingGatewayServiceAccountId,
|
|
32
33
|
staticPublisherServiceAccountId,
|
|
33
34
|
} from "../../dist/gcp-names.js";
|
|
34
35
|
import { createSiteUrlMap } from "../../dist/gcp-url-map.js";
|
|
@@ -513,9 +514,10 @@ new gcp.storage.BucketIAMMember(
|
|
|
513
514
|
|
|
514
515
|
// v0.2.78 — private staging bucket. Stage uploads here; Confirm-publish
|
|
515
516
|
// server-side copies to the public static bucket. Never has allUsers
|
|
516
|
-
// binding
|
|
517
|
-
//
|
|
518
|
-
//
|
|
517
|
+
// binding: since #1330 the staged build is reachable ONLY through the
|
|
518
|
+
// staging gateway (the staging host, behind the staging gate), whose
|
|
519
|
+
// account may read this bucket and nothing else. The public CDN never
|
|
520
|
+
// serves from it.
|
|
519
521
|
const stagingBucket = new gcp.storage.Bucket(
|
|
520
522
|
`${namePrefix}-staging`,
|
|
521
523
|
{
|
|
@@ -570,9 +572,24 @@ const mcpAuthProxySa = new gcp.serviceaccount.Account(
|
|
|
570
572
|
},
|
|
571
573
|
opts,
|
|
572
574
|
);
|
|
575
|
+
// #1330 — the staging gateway's own SA and (below) its secretAccessor on
|
|
576
|
+
// its one login password, plus the telemetry roles. Its service, its read
|
|
577
|
+
// access to the staging bucket and its edge (gcp: NEG, backend, host rule,
|
|
578
|
+
// certificate; gcp-firebase: the second Hosting site) are the CLI's
|
|
579
|
+
// (staging-gateway-setup.ts, after the first `up` and in every converge).
|
|
580
|
+
const stagingGatewaySa = new gcp.serviceaccount.Account(
|
|
581
|
+
`${namePrefix}-staging-gateway-sa`,
|
|
582
|
+
{
|
|
583
|
+
accountId: stagingGatewayServiceAccountId(env),
|
|
584
|
+
displayName: `Caelo ${env} staging gateway`,
|
|
585
|
+
createIgnoreAlreadyExists: true,
|
|
586
|
+
},
|
|
587
|
+
opts,
|
|
588
|
+
);
|
|
573
589
|
const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
|
|
574
590
|
admin: runSa,
|
|
575
591
|
gateway: gatewaySa,
|
|
592
|
+
"staging-gateway": stagingGatewaySa,
|
|
576
593
|
"mcp-auth-proxy": mcpAuthProxySa,
|
|
577
594
|
};
|
|
578
595
|
|
|
@@ -582,6 +599,7 @@ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
|
|
|
582
599
|
const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
|
|
583
600
|
admin: [],
|
|
584
601
|
gateway: [],
|
|
602
|
+
"staging-gateway": [],
|
|
585
603
|
"mcp-auth-proxy": [],
|
|
586
604
|
};
|
|
587
605
|
for (const binding of runtimeSecretBindings()) {
|
|
@@ -696,6 +714,24 @@ new gcp.projects.IAMMember(
|
|
|
696
714
|
},
|
|
697
715
|
opts,
|
|
698
716
|
);
|
|
717
|
+
new gcp.projects.IAMMember(
|
|
718
|
+
`${namePrefix}-staging-gateway-log-writer`,
|
|
719
|
+
{
|
|
720
|
+
project,
|
|
721
|
+
role: "roles/logging.logWriter",
|
|
722
|
+
member: pulumi.interpolate`serviceAccount:${stagingGatewaySa.email}`,
|
|
723
|
+
},
|
|
724
|
+
opts,
|
|
725
|
+
);
|
|
726
|
+
new gcp.projects.IAMMember(
|
|
727
|
+
`${namePrefix}-staging-gateway-metric-writer`,
|
|
728
|
+
{
|
|
729
|
+
project,
|
|
730
|
+
role: "roles/monitoring.metricWriter",
|
|
731
|
+
member: pulumi.interpolate`serviceAccount:${stagingGatewaySa.email}`,
|
|
732
|
+
},
|
|
733
|
+
opts,
|
|
734
|
+
);
|
|
699
735
|
|
|
700
736
|
// `static-publisher` SA used by `bunx @caelo-cms/provisioning deploy` to
|
|
701
737
|
// upload the static-generator output to the bucket. Pre-v0.2.78 this
|
|
@@ -759,8 +795,8 @@ function imageTag(service: string): pulumi.Output<string> {
|
|
|
759
795
|
}
|
|
760
796
|
|
|
761
797
|
interface CloudRunArgs {
|
|
762
|
-
/** The MCP-Auth-Proxy
|
|
763
|
-
readonly serviceName: Exclude<CloudRunSlug, "mcp-auth-proxy">;
|
|
798
|
+
/** The MCP-Auth-Proxy and the staging gateway are CLI-owned on this stack (see their SAs). */
|
|
799
|
+
readonly serviceName: Exclude<CloudRunSlug, "mcp-auth-proxy" | "staging-gateway">;
|
|
764
800
|
readonly minInstances: number;
|
|
765
801
|
readonly maxInstances: number;
|
|
766
802
|
readonly memory: string;
|
|
@@ -1227,6 +1263,7 @@ const urlMap = createSiteUrlMap(
|
|
|
1227
1263
|
{
|
|
1228
1264
|
domain,
|
|
1229
1265
|
adminDomain,
|
|
1266
|
+
stagingHost,
|
|
1230
1267
|
staticBackend: staticBackendBucket.id,
|
|
1231
1268
|
gatewayBackend: gatewayBackendService.id,
|
|
1232
1269
|
adminBackend: adminBackendService.id,
|
|
@@ -1261,10 +1298,14 @@ const adminCert = new gcp.compute.ManagedSslCertificate(
|
|
|
1261
1298
|
opts,
|
|
1262
1299
|
);
|
|
1263
1300
|
|
|
1301
|
+
// #1330 — the staging host's own certificate is the CLI's
|
|
1302
|
+
// (staging-gateway-setup.ts, `caelo-<env>-staging-cert`), attached next to
|
|
1303
|
+
// these two after the first `up` and by every converge, so the proxy's
|
|
1304
|
+
// certificate list is left to it after creation.
|
|
1264
1305
|
const httpsProxy = new gcp.compute.TargetHttpsProxy(
|
|
1265
1306
|
`${namePrefix}-https-proxy`,
|
|
1266
1307
|
{ urlMap: urlMap.id, sslCertificates: [adminCert.id, managedCert.id] },
|
|
1267
|
-
opts,
|
|
1308
|
+
{ ...opts, ignoreChanges: ["sslCertificates"] },
|
|
1268
1309
|
);
|
|
1269
1310
|
|
|
1270
1311
|
new gcp.compute.GlobalForwardingRule(
|
|
@@ -33,12 +33,14 @@ import * as random from "@pulumi/random";
|
|
|
33
33
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
34
34
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
35
35
|
import {
|
|
36
|
+
firebaseStagingSiteId,
|
|
36
37
|
gatewayServiceAccountId,
|
|
37
38
|
gcpBucketName,
|
|
38
39
|
gcpSecretId,
|
|
39
40
|
MCP_IAP_SERVICE_ACCOUNT_ID,
|
|
40
41
|
mcpAuthProxyServiceAccountId,
|
|
41
42
|
runServiceAccountId,
|
|
43
|
+
stagingGatewayServiceAccountId,
|
|
42
44
|
} from "../../dist/gcp-names.js";
|
|
43
45
|
import {
|
|
44
46
|
DEFAULT_ADMIN_MAX_INSTANCES,
|
|
@@ -65,7 +67,6 @@ import {
|
|
|
65
67
|
defaultStagingHost,
|
|
66
68
|
fixedCloudRunServiceName,
|
|
67
69
|
gatewayEnvContract,
|
|
68
|
-
INTERIM_FORWARD_ORIGIN_ENV,
|
|
69
70
|
MCP_AUTH_PROXY_TIMEOUT_SECONDS,
|
|
70
71
|
mcpAuthProxyEnvContract,
|
|
71
72
|
REMOTE_MCP_IDENTITY_CONFIG,
|
|
@@ -482,9 +483,24 @@ const mcpAuthProxySa = new gcp.serviceaccount.Account(
|
|
|
482
483
|
},
|
|
483
484
|
opts,
|
|
484
485
|
);
|
|
486
|
+
// #1330 — the staging gateway's own SA and (below) its secretAccessor on
|
|
487
|
+
// its one login password, plus the telemetry roles. Its service, its read
|
|
488
|
+
// access to the staging bucket and its edge (gcp: NEG, backend, host rule,
|
|
489
|
+
// certificate; gcp-firebase: the second Hosting site) are the CLI's
|
|
490
|
+
// (staging-gateway-setup.ts, after the first `up` and in every converge).
|
|
491
|
+
const stagingGatewaySa = new gcp.serviceaccount.Account(
|
|
492
|
+
`${namePrefix}-staging-gateway-sa`,
|
|
493
|
+
{
|
|
494
|
+
accountId: stagingGatewayServiceAccountId(env),
|
|
495
|
+
displayName: `Caelo ${env} staging gateway`,
|
|
496
|
+
createIgnoreAlreadyExists: true,
|
|
497
|
+
},
|
|
498
|
+
opts,
|
|
499
|
+
);
|
|
485
500
|
const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
|
|
486
501
|
admin: runSa,
|
|
487
502
|
gateway: gatewaySa,
|
|
503
|
+
"staging-gateway": stagingGatewaySa,
|
|
488
504
|
"mcp-auth-proxy": mcpAuthProxySa,
|
|
489
505
|
};
|
|
490
506
|
|
|
@@ -494,6 +510,7 @@ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
|
|
|
494
510
|
const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
|
|
495
511
|
admin: [],
|
|
496
512
|
gateway: [],
|
|
513
|
+
"staging-gateway": [],
|
|
497
514
|
"mcp-auth-proxy": [],
|
|
498
515
|
};
|
|
499
516
|
for (const binding of runtimeSecretBindings()) {
|
|
@@ -593,6 +610,24 @@ new gcp.projects.IAMMember(
|
|
|
593
610
|
},
|
|
594
611
|
opts,
|
|
595
612
|
);
|
|
613
|
+
new gcp.projects.IAMMember(
|
|
614
|
+
`${namePrefix}-staging-gateway-log-writer`,
|
|
615
|
+
{
|
|
616
|
+
project,
|
|
617
|
+
role: "roles/logging.logWriter",
|
|
618
|
+
member: pulumi.interpolate`serviceAccount:${stagingGatewaySa.email}`,
|
|
619
|
+
},
|
|
620
|
+
opts,
|
|
621
|
+
);
|
|
622
|
+
new gcp.projects.IAMMember(
|
|
623
|
+
`${namePrefix}-staging-gateway-metric-writer`,
|
|
624
|
+
{
|
|
625
|
+
project,
|
|
626
|
+
role: "roles/monitoring.metricWriter",
|
|
627
|
+
member: pulumi.interpolate`serviceAccount:${stagingGatewaySa.email}`,
|
|
628
|
+
},
|
|
629
|
+
opts,
|
|
630
|
+
);
|
|
596
631
|
|
|
597
632
|
// =========================================================================
|
|
598
633
|
// Tier 1 — Firebase Hosting site
|
|
@@ -805,12 +840,10 @@ const gatewaySvc = cloudRunService({
|
|
|
805
840
|
memory: "512Mi",
|
|
806
841
|
// Firebase Hosting cuts a rewritten request at 60 s anyway.
|
|
807
842
|
timeout: "300s",
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
// passes its rate limits, captcha and honeypot; staging requests pass the
|
|
813
|
-
// staging access gate.
|
|
843
|
+
// All traffic: the Firebase rewrite arrives from Google's edge, and it is
|
|
844
|
+
// invokable by allUsers anyway (data-gateway-public-invoker below): every
|
|
845
|
+
// /api request passes its rate limits, captcha and honeypot. Staging has a
|
|
846
|
+
// gateway of its own since #1330 (staging-gateway-setup.ts).
|
|
814
847
|
ingress: "INGRESS_TRAFFIC_ALL",
|
|
815
848
|
contractEnv: [...gatewayEnvContract(envContractInputs), ...poolBudget.env.gateway],
|
|
816
849
|
});
|
|
@@ -832,15 +865,11 @@ const mcpAuthProxySvc = cloudRunService({
|
|
|
832
865
|
// MCP clients call its run.app URL, never the Firebase rewrite (which
|
|
833
866
|
// buffers streams and cuts at 60 s).
|
|
834
867
|
timeout: `${MCP_AUTH_PROXY_TIMEOUT_SECONDS}s`,
|
|
835
|
-
// Public: MCP clients reach it directly
|
|
836
|
-
// domain mapping (#650) still points at this service too, and the proxy
|
|
837
|
-
// forwards that traffic to the data gateway (INTERIM_FORWARD_ORIGIN_ENV).
|
|
868
|
+
// Public: MCP clients reach it directly.
|
|
838
869
|
ingress: "INGRESS_TRAFFIC_ALL",
|
|
839
870
|
contractEnv: [
|
|
840
871
|
...mcpAuthProxyEnvContract({ provider: "gcp-firebase", env }),
|
|
841
872
|
...remoteMcpEnv["mcp-auth-proxy"],
|
|
842
|
-
// FIXME(issue #1331): goes with the staging cutover.
|
|
843
|
-
{ name: INTERIM_FORWARD_ORIGIN_ENV, value: gatewaySvc.uri },
|
|
844
873
|
],
|
|
845
874
|
});
|
|
846
875
|
|
|
@@ -861,6 +890,9 @@ const adminSvc = cloudRunService({
|
|
|
861
890
|
...adminEnvContract({
|
|
862
891
|
...envContractInputs,
|
|
863
892
|
firebaseSiteId,
|
|
893
|
+
firebaseStagingSiteId: pulumi
|
|
894
|
+
.output(firebaseSiteId)
|
|
895
|
+
.apply((id) => firebaseStagingSiteId(env, id)),
|
|
864
896
|
gatewayService: gatewaySvc.name,
|
|
865
897
|
}),
|
|
866
898
|
...remoteMcpEnv.admin,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
COMPOSE_ADMIN_UPSTREAM,
|
|
31
31
|
COMPOSE_GATEWAY_UPSTREAM,
|
|
32
32
|
COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|
|
33
|
+
COMPOSE_STAGING_GATEWAY_UPSTREAM,
|
|
33
34
|
composeProjectName,
|
|
34
35
|
generateDockerCompose,
|
|
35
36
|
SITE_OUTPUT_ROOT,
|
|
@@ -89,6 +90,9 @@ const publicRolePassword = pulumi.secret(randomHex(32));
|
|
|
89
90
|
const publicStagingRolePassword = pulumi.secret(randomHex(32));
|
|
90
91
|
// P18 — the KEK that encrypts stored provider keys.
|
|
91
92
|
const caeloSecretKek = pulumi.secret(randomHex(32));
|
|
93
|
+
// #650 / #1330 — the staging gate's key (admin only; the admin publishes it
|
|
94
|
+
// to the staging gateway through the staging projection).
|
|
95
|
+
const stagingGateSecret = pulumi.secret(randomHex(32));
|
|
92
96
|
|
|
93
97
|
const composeYaml = pulumi
|
|
94
98
|
.all([
|
|
@@ -97,10 +101,13 @@ const composeYaml = pulumi
|
|
|
97
101
|
publicRolePassword,
|
|
98
102
|
publicStagingRolePassword,
|
|
99
103
|
caeloSecretKek,
|
|
104
|
+
stagingGateSecret,
|
|
100
105
|
])
|
|
101
|
-
.apply(([pgPw, adminPw, publicPw, publicStagingPw, kek]) =>
|
|
106
|
+
.apply(([pgPw, adminPw, publicPw, publicStagingPw, kek, gateSecret]) =>
|
|
102
107
|
generateDockerCompose({
|
|
103
108
|
domain,
|
|
109
|
+
stagingHost,
|
|
110
|
+
stagingGateSecret: gateSecret,
|
|
104
111
|
postgresPassword: pgPw,
|
|
105
112
|
rolePasswords: { admin: adminPw, public: publicPw, publicStaging: publicStagingPw },
|
|
106
113
|
caeloSecretKek: kek,
|
|
@@ -126,7 +133,7 @@ const caddyConf = generateCaddyfile({
|
|
|
126
133
|
ownerEmail,
|
|
127
134
|
tls: "acme",
|
|
128
135
|
publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
|
|
129
|
-
|
|
136
|
+
stagingGatewayUpstream: COMPOSE_STAGING_GATEWAY_UPSTREAM,
|
|
130
137
|
adminUpstream: COMPOSE_ADMIN_UPSTREAM,
|
|
131
138
|
gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
|
|
132
139
|
mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
|