@caelo-cms/provisioning 0.11.13 → 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 -46
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +4 -101
- 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 +18 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +5 -39
- 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/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 -156
- package/src/admin-domain.ts +4 -117
- 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 +4 -3
- package/src/infra-contract.ts +18 -1
- package/src/lifecycle.ts +8 -52
- 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/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/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");
|
|
@@ -39,6 +39,7 @@ describe("infrastructure contract steps", () => {
|
|
|
39
39
|
expect(INFRA_CONTRACT_STEPS[17]).toContain("#1329");
|
|
40
40
|
expect(INFRA_CONTRACT_STEPS[18]).toContain("#1140");
|
|
41
41
|
expect(INFRA_CONTRACT_STEPS[19]).toContain("JSON");
|
|
42
|
+
expect(INFRA_CONTRACT_STEPS[20]).toContain("#1330");
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
it("lists the pending steps oldest first", () => {
|
|
@@ -49,6 +50,6 @@ describe("infrastructure contract steps", () => {
|
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
it("names a version a newer release added by number", () => {
|
|
52
|
-
expect(infraStepsText(
|
|
53
|
+
expect(infraStepsText(20, 21)).toBe("infrastructure step 21 (see that release's notes)");
|
|
53
54
|
});
|
|
54
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):
|
|
@@ -153,6 +153,22 @@ export const INFRA_CONTRACT_VERSION = 19;
|
|
|
153
153
|
// 19 — the scheduler workflow's tick is a JSON POST. Without a content type
|
|
154
154
|
// SvelteKit refused every tick of contract 18 with 403, so no periodic
|
|
155
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.
|
|
156
172
|
|
|
157
173
|
/**
|
|
158
174
|
* What each contract version adds, in operator terms — the upgrader's
|
|
@@ -179,6 +195,7 @@ export const INFRA_CONTRACT_STEPS: Readonly<Record<number, string>> = {
|
|
|
179
195
|
17: "the staging site's own database and its gateway login, kept apart from the live data (#1329)",
|
|
180
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)",
|
|
181
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)",
|
|
182
199
|
};
|
|
183
200
|
|
|
184
201
|
/**
|
package/src/lifecycle.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { join } from "node:path";
|
|
|
20
20
|
import { provisioningUpgradeCommand } from "@caelo-cms/shared";
|
|
21
21
|
import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
|
|
22
22
|
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
23
|
-
import { stagingDomainState } from "./admin-domain.js";
|
|
24
23
|
import { activeAccount, checkGcloudAuth, gcloud } from "./gcloud.js";
|
|
25
24
|
import { type GcloudRunner, realSleep } from "./gcloud-retry.js";
|
|
26
25
|
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
@@ -70,12 +69,7 @@ import {
|
|
|
70
69
|
resolveBackend,
|
|
71
70
|
} from "./state-backend.js";
|
|
72
71
|
import { acquireStateLock, realSyncGcloud, releaseOnSignal } from "./state-store.js";
|
|
73
|
-
import {
|
|
74
|
-
type AppRollPlan,
|
|
75
|
-
type ReleaseImage,
|
|
76
|
-
ROLL_ORDER,
|
|
77
|
-
type RollSlug,
|
|
78
|
-
} from "./upgrade/app-roll.js";
|
|
72
|
+
import type { AppRollPlan, RollSlug } from "./upgrade/app-roll.js";
|
|
79
73
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
80
74
|
import { findServiceUrl, lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
81
75
|
import { createIapHealthProbe, type HealthProbe } from "./upgrade/health.js";
|
|
@@ -85,12 +79,10 @@ import {
|
|
|
85
79
|
canonicalReleaseTag,
|
|
86
80
|
ensureCosignAvailable,
|
|
87
81
|
fetchLatestReleaseTag,
|
|
88
|
-
RELEASE_REGISTRY,
|
|
89
|
-
releaseImageRef,
|
|
90
82
|
releaseTag,
|
|
91
83
|
releaseVersionForDigest,
|
|
92
84
|
resolveOpsImageRef,
|
|
93
|
-
|
|
85
|
+
resolveReleaseImages,
|
|
94
86
|
verifyCosignAll,
|
|
95
87
|
} from "./upgrade/release-images.js";
|
|
96
88
|
import { clackReporter } from "./upgrade/reporter.js";
|
|
@@ -464,32 +456,6 @@ export async function upgradeCommand(opts: UpgradeOpts = {}): Promise<void> {
|
|
|
464
456
|
);
|
|
465
457
|
}
|
|
466
458
|
|
|
467
|
-
/**
|
|
468
|
-
* Resolve both release images' digests from the public registry. Via the
|
|
469
|
-
* registry's anonymous V2 API, not `gcloud artifacts docker tags list`: that
|
|
470
|
-
* needs IAM read on Caelo's registry project, which operators don't have.
|
|
471
|
-
*/
|
|
472
|
-
async function resolveReleaseImages(
|
|
473
|
-
tag: string,
|
|
474
|
-
): Promise<{ ok: true; images: Record<RollSlug, ReleaseImage> } | { ok: false; error: string }> {
|
|
475
|
-
const images: Partial<Record<RollSlug, ReleaseImage>> = {};
|
|
476
|
-
for (const slug of ROLL_ORDER) {
|
|
477
|
-
const res = await resolveTagDigest(slug, tag);
|
|
478
|
-
if (!res.ok) {
|
|
479
|
-
const r = RELEASE_REGISTRY;
|
|
480
|
-
return {
|
|
481
|
-
ok: false,
|
|
482
|
-
error:
|
|
483
|
-
`Couldn't resolve image digest for ${slug}:${tag}: the public registry returned ${res.reason}.\n` +
|
|
484
|
-
`Verify the tag exists at https://${r.region}-docker.pkg.dev/${r.project}/${r.repo}/${slug}:${tag}\n` +
|
|
485
|
-
"(latest releases live at https://github.com/caelo-cms/caelo-cms/releases — pass --version vX.Y.Z to pin.)",
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
images[slug] = { digest: res.digest, imageRef: releaseImageRef(slug, res.digest) };
|
|
489
|
-
}
|
|
490
|
-
return { ok: true, images: images as Record<RollSlug, ReleaseImage> };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
459
|
/**
|
|
494
460
|
* `upgrade`'s step of the keyless provisioner migration (#696,
|
|
495
461
|
* provisioner-identity.ts) — a converge step (upgrade/converge.ts runs it
|
|
@@ -686,22 +652,12 @@ async function upgradeLockedInstall(
|
|
|
686
652
|
// #696 — upgrade never moves Pulumi state; it says how to.
|
|
687
653
|
const stateNotice = localStateNotice(meta);
|
|
688
654
|
if (stateNotice) note(stateNotice, "Pulumi state is on this machine");
|
|
689
|
-
if (ok
|
|
690
|
-
// #
|
|
691
|
-
//
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
log.info(`Protected staging site: https://${stagingHost}/`);
|
|
696
|
-
} else {
|
|
697
|
-
const unknown =
|
|
698
|
-
staging.status === "unknown"
|
|
699
|
-
? ` (could not check whether ${bold(stagingHost)} is mapped: ${staging.reason})`
|
|
700
|
-
: "";
|
|
701
|
-
log.info(
|
|
702
|
-
`Protected staging site: run ${bold("cms-provision staging-domain enable")} once to serve the staging host${unknown} (it maps the host the admin uses — ${bold(stagingHost)} unless changed there —, turns on password protection, prints the login and the DNS record). Host and access: admin → Settings → Domains / Staging access.`,
|
|
703
|
-
);
|
|
704
|
-
}
|
|
655
|
+
if (ok) {
|
|
656
|
+
// #1330 — the converge routed the staging host to the staging gateway
|
|
657
|
+
// and printed its DNS records (Settings → Updates shows them as well).
|
|
658
|
+
log.info(
|
|
659
|
+
`Protected staging site: https://${stagingHost}/ — served by the staging gateway behind the staging gate. Password protection and other access: admin → Settings → Staging access (or ${bold("cms-provision staging-domain enable")}).`,
|
|
660
|
+
);
|
|
705
661
|
}
|
|
706
662
|
return ok;
|
|
707
663
|
}
|
|
@@ -44,8 +44,17 @@ const firebase: ReleaseServicesTarget = {
|
|
|
44
44
|
region: "europe-west1",
|
|
45
45
|
env: "production",
|
|
46
46
|
domain: "acme.com",
|
|
47
|
+
stagingHost: "staging.acme.com",
|
|
47
48
|
mcpAuthProxyImageRef: `${IMAGE}/mcp-auth-proxy@sha256:${"c".repeat(64)}`,
|
|
49
|
+
gatewayImageRef: `${IMAGE}/gateway@sha256:${"b".repeat(64)}`,
|
|
48
50
|
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The outcomes of the #1328 part; the staging gateway's (#1330) come after
|
|
54
|
+
* them and are covered by staging-gateway-setup.test.ts.
|
|
55
|
+
*/
|
|
56
|
+
const proxyPart = (outcomes: readonly { id: string; status: string }[]) =>
|
|
57
|
+
outcomes.filter((o) => !/staging gateway/.test(o.id)).map((o) => o.status);
|
|
49
58
|
const gcp: ReleaseServicesTarget = { ...firebase, provider: "gcp" };
|
|
50
59
|
|
|
51
60
|
/** The old gateway service as `gcloud run services describe --format=json` shows it. */
|
|
@@ -210,7 +219,7 @@ describe("ensureReleaseServices", () => {
|
|
|
210
219
|
};
|
|
211
220
|
},
|
|
212
221
|
});
|
|
213
|
-
expect(outcomes
|
|
222
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "applied"]);
|
|
214
223
|
expect(calls.filter((c) => c.startsWith("secrets add-iam-policy-binding"))).toEqual([
|
|
215
224
|
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=serviceAccount:${PROXY_SA} --role=roles/secretmanager.secretAccessor --condition=None --quiet`,
|
|
216
225
|
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=serviceAccount:${PROXY_SA} --role=roles/secretmanager.secretAccessor --condition=None --quiet`,
|
|
@@ -237,7 +246,7 @@ describe("ensureReleaseServices", () => {
|
|
|
237
246
|
"run services describe caelo-production-data-gateway": ok("caelo-production-data-gateway\n"),
|
|
238
247
|
});
|
|
239
248
|
const outcomes = await ensureReleaseServices(firebase, { run });
|
|
240
|
-
expect(outcomes
|
|
249
|
+
expect(proxyPart(outcomes)).toEqual(["present", "present", "present", "present"]);
|
|
241
250
|
expect(calls.some((c) => c.includes("replace") || c.includes("create"))).toBe(false);
|
|
242
251
|
});
|
|
243
252
|
|
|
@@ -267,7 +276,7 @@ describe("ensureReleaseServices", () => {
|
|
|
267
276
|
"run deploy caelo-production-mcp-auth-proxy": ok(),
|
|
268
277
|
});
|
|
269
278
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
270
|
-
expect(outcomes
|
|
279
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "applied"]);
|
|
271
280
|
expect(calls).toContain(gcpProxyDeployArgs(gcp).join(" "));
|
|
272
281
|
});
|
|
273
282
|
|
|
@@ -280,7 +289,7 @@ describe("ensureReleaseServices", () => {
|
|
|
280
289
|
),
|
|
281
290
|
});
|
|
282
291
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
283
|
-
expect(outcomes.at(-1)
|
|
292
|
+
expect(proxyPart(outcomes).at(-1)).toBe("present");
|
|
284
293
|
expect(calls.some((c) => c.startsWith("run deploy"))).toBe(false);
|
|
285
294
|
});
|
|
286
295
|
|
|
@@ -292,7 +301,7 @@ describe("ensureReleaseServices", () => {
|
|
|
292
301
|
),
|
|
293
302
|
});
|
|
294
303
|
const outcomes = await ensureReleaseServices(gcp, { run });
|
|
295
|
-
expect(outcomes
|
|
304
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "failed"]);
|
|
296
305
|
expect(calls.some((c) => c.startsWith("run "))).toBe(false);
|
|
297
306
|
});
|
|
298
307
|
|
|
@@ -320,7 +329,7 @@ describe("ensureReleaseServices", () => {
|
|
|
320
329
|
pauses.push(ms);
|
|
321
330
|
},
|
|
322
331
|
});
|
|
323
|
-
expect(outcomes
|
|
332
|
+
expect(proxyPart(outcomes)).toEqual(["applied", "present", "present", "present"]);
|
|
324
333
|
expect(pauses.length).toBe(2);
|
|
325
334
|
expect(calls.filter((c) => c.startsWith("secrets add-iam-policy-binding"))).toHaveLength(2);
|
|
326
335
|
});
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
serviceSecrets,
|
|
50
50
|
} from "./stack-contract.js";
|
|
51
51
|
import { listFlag } from "./stack-converge.js";
|
|
52
|
+
import { ensureStagingGatewayFromLiveGateway } from "./staging-gateway-setup.js";
|
|
52
53
|
import { lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
53
54
|
|
|
54
55
|
/** The install the services are ensured on. */
|
|
@@ -61,8 +62,15 @@ export interface ReleaseServicesTarget {
|
|
|
61
62
|
readonly domain: string;
|
|
62
63
|
/** The release's digest-pinned MCP-Auth-Proxy image (`…/mcp-auth-proxy@sha256:…`). */
|
|
63
64
|
readonly mcpAuthProxyImageRef: string;
|
|
65
|
+
/** #1330 — the release's digest-pinned gateway image, which the staging gateway runs too. */
|
|
66
|
+
readonly gatewayImageRef: string;
|
|
67
|
+
/** #1330 — the staging host (install.json `stagingDomain`). */
|
|
68
|
+
readonly stagingHost: string;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/** What the `gcp` proxy's own service needs. */
|
|
72
|
+
export type ProxyServiceTarget = Omit<ReleaseServicesTarget, "gatewayImageRef" | "stagingHost">;
|
|
73
|
+
|
|
66
74
|
export interface ReleaseServicesDeps {
|
|
67
75
|
readonly run?: GcloudRunner;
|
|
68
76
|
/** Writes a service spec for `gcloud run services replace`; returns its path and a cleanup. */
|
|
@@ -95,6 +103,10 @@ export async function ensureReleaseServices(
|
|
|
95
103
|
? await ensureDataGatewayService(target, run, deps.writeSpecFile ?? writeTempSpec)
|
|
96
104
|
: await ensureMcpAuthProxyServiceGcp(target, run),
|
|
97
105
|
);
|
|
106
|
+
if (outcomes.some((o) => o.status === "failed")) return outcomes;
|
|
107
|
+
outcomes.push(
|
|
108
|
+
...(await ensureStagingGatewayFromLiveGateway(target, { run, sleep: deps.sleep ?? realSleep })),
|
|
109
|
+
);
|
|
98
110
|
return outcomes;
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -317,7 +329,7 @@ function gcpProxyEnv(target: Pick<ReleaseServicesTarget, "env" | "domain">): Clo
|
|
|
317
329
|
* invokable without a Google identity (MCP clients have none), request-based
|
|
318
330
|
* CPU, no VPC, and its Google sign-in client file as a secret volume.
|
|
319
331
|
*/
|
|
320
|
-
export function gcpProxyDeployArgs(target:
|
|
332
|
+
export function gcpProxyDeployArgs(target: ProxyServiceTarget): string[] {
|
|
321
333
|
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
|
322
334
|
const env = gcpProxyEnv(target);
|
|
323
335
|
const plain = env.flatMap((e) => ("value" in e ? [`${e.name}=${e.value}`] : []));
|
|
@@ -356,7 +368,7 @@ export function gcpProxyDeployArgs(target: ReleaseServicesTarget): string[] {
|
|
|
356
368
|
* app-roll moves it onto each release's image after that.
|
|
357
369
|
*/
|
|
358
370
|
export async function ensureMcpAuthProxyServiceGcp(
|
|
359
|
-
target:
|
|
371
|
+
target: ProxyServiceTarget,
|
|
360
372
|
run: GcloudRunner,
|
|
361
373
|
): Promise<EnsureOutcome> {
|
|
362
374
|
const name = fixedCloudRunServiceName("gcp", target.env, "mcp-auth-proxy") as string;
|
package/src/release-manifest.ts
CHANGED
|
@@ -254,7 +254,7 @@ export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "dige
|
|
|
254
254
|
| {
|
|
255
255
|
readonly ok: true;
|
|
256
256
|
readonly images: Readonly<
|
|
257
|
-
Record<"admin" | "gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
257
|
+
Record<"admin" | "gateway" | "staging-gateway" | "mcp-auth-proxy", ManifestServiceImage>
|
|
258
258
|
>;
|
|
259
259
|
}
|
|
260
260
|
| { readonly ok: false; readonly error: string } {
|
|
@@ -274,6 +274,8 @@ export function manifestServiceImages(m: Pick<ReleaseManifest, "version" | "dige
|
|
|
274
274
|
images: {
|
|
275
275
|
admin: image("admin", m.digests.admin),
|
|
276
276
|
gateway: image("gateway", m.digests.gateway),
|
|
277
|
+
// #1330 — the staging gateway runs the live gateway's image.
|
|
278
|
+
"staging-gateway": image("gateway", m.digests.gateway),
|
|
277
279
|
"mcp-auth-proxy": image("mcp-auth-proxy", proxy),
|
|
278
280
|
},
|
|
279
281
|
};
|
package/src/remote-mcp.test.ts
CHANGED
|
@@ -413,6 +413,7 @@ const firebase: RemoteMcpTarget = {
|
|
|
413
413
|
services: {
|
|
414
414
|
admin: "caelo-production-admin-aaa",
|
|
415
415
|
gateway: "caelo-production-data-gateway",
|
|
416
|
+
"staging-gateway": "caelo-production-staging-gateway",
|
|
416
417
|
"mcp-auth-proxy": "caelo-production-gateway-bbb",
|
|
417
418
|
},
|
|
418
419
|
};
|
|
@@ -606,7 +607,10 @@ describe("checkRemoteMcpIdentityEnv (#1082)", () => {
|
|
|
606
607
|
),
|
|
607
608
|
});
|
|
608
609
|
const r = await checkRemoteMcpIdentityEnv(firebase, identity, { run });
|
|
609
|
-
expect(r).toEqual({
|
|
610
|
+
expect(r).toEqual({
|
|
611
|
+
ok: true,
|
|
612
|
+
drift: { admin: [], gateway: [], "staging-gateway": [], "mcp-auth-proxy": [] },
|
|
613
|
+
});
|
|
610
614
|
expect(calls.some((c) => c.startsWith("run services update"))).toBe(false);
|
|
611
615
|
});
|
|
612
616
|
|
package/src/remote-mcp.ts
CHANGED
|
@@ -345,8 +345,17 @@ export async function checkRemoteMcpIdentityEnv(
|
|
|
345
345
|
): Promise<RemoteMcpResult<{ readonly drift: Record<CloudRunSlug, EnvChange[]> }>> {
|
|
346
346
|
const run = deps.run ?? defaultGcloud;
|
|
347
347
|
const desired = remoteMcpIdentityEnv(identity);
|
|
348
|
-
const drift: Record<CloudRunSlug, EnvChange[]> = {
|
|
348
|
+
const drift: Record<CloudRunSlug, EnvChange[]> = {
|
|
349
|
+
admin: [],
|
|
350
|
+
gateway: [],
|
|
351
|
+
"staging-gateway": [],
|
|
352
|
+
"mcp-auth-proxy": [],
|
|
353
|
+
};
|
|
349
354
|
for (const slug of CLOUD_RUN_SLUGS) {
|
|
355
|
+
// Nothing to check where the identity names nothing (the visitor
|
|
356
|
+
// gateways), and the staging gateway is the CLI's (#1330), not the
|
|
357
|
+
// stack's: it may not exist yet right after a first `up`.
|
|
358
|
+
if (desired[slug].length === 0) continue;
|
|
350
359
|
const described = await describeService(target, target.services[slug], run);
|
|
351
360
|
if (!described.ok) return described;
|
|
352
361
|
const plan = planEnvUpdate(liveContainerEnv(described.json), desired[slug]);
|
|
@@ -323,7 +323,12 @@ describe("rotateRuntimeSecret", () => {
|
|
|
323
323
|
projectId: "acme",
|
|
324
324
|
region: "europe-west1",
|
|
325
325
|
env: "production",
|
|
326
|
-
services: {
|
|
326
|
+
services: {
|
|
327
|
+
admin: "adm-svc",
|
|
328
|
+
gateway: "gw-svc",
|
|
329
|
+
"staging-gateway": "stg-svc",
|
|
330
|
+
"mcp-auth-proxy": "px-svc",
|
|
331
|
+
},
|
|
327
332
|
sqlInstance: "caelo-production-pg-1",
|
|
328
333
|
};
|
|
329
334
|
const ref = (name: string, secret: string, version = "latest") => ({
|
|
@@ -347,6 +352,10 @@ describe("rotateRuntimeSecret", () => {
|
|
|
347
352
|
"run services describe gw-svc": [
|
|
348
353
|
service([ref("PUBLIC_DATABASE_PASSWORD", "public-role-password")]),
|
|
349
354
|
],
|
|
355
|
+
// #1330 — the staging gateway reads its own login password.
|
|
356
|
+
"run services describe stg-svc": [
|
|
357
|
+
service([ref("PUBLIC_DATABASE_PASSWORD", "public-staging-role-password")]),
|
|
358
|
+
],
|
|
350
359
|
// #1328 — the proxy reads none of the rotatable database secrets.
|
|
351
360
|
"run services describe px-svc": [
|
|
352
361
|
service([ref("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]),
|
package/src/runtime-secrets.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
gcpSecretId,
|
|
31
31
|
mcpAuthProxyServiceAccountEmail,
|
|
32
32
|
mcpAuthProxyServiceAccountId,
|
|
33
|
+
stagingGatewayServiceAccountEmail,
|
|
34
|
+
stagingGatewayServiceAccountId,
|
|
33
35
|
} from "./gcp-names.js";
|
|
34
36
|
import {
|
|
35
37
|
CLI_GENERATED_SECRETS,
|
|
@@ -123,6 +125,25 @@ export function ensureMcpAuthProxyServiceAccount(
|
|
|
123
125
|
);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* #1330 — create the staging gateway's run SA unless it exists. Its rights:
|
|
130
|
+
* `secretAccessor` on `public-staging-role-password` (runtimeSecretBindings),
|
|
131
|
+
* the log and metric writer, and object viewer on the staging bucket
|
|
132
|
+
* (staging-gateway-setup.ts) — nothing of the live site's.
|
|
133
|
+
*/
|
|
134
|
+
export function ensureStagingGatewayServiceAccount(
|
|
135
|
+
install: { readonly projectId: string; readonly env: string },
|
|
136
|
+
deps: RuntimeDeps = {},
|
|
137
|
+
): Promise<EnsureOutcome> {
|
|
138
|
+
return ensureServiceAccount(
|
|
139
|
+
install.projectId,
|
|
140
|
+
stagingGatewayServiceAccountId(install.env),
|
|
141
|
+
stagingGatewayServiceAccountEmail(install.projectId, install.env),
|
|
142
|
+
`Caelo ${install.env} staging gateway`,
|
|
143
|
+
deps,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
async function ensureServiceAccount(
|
|
127
148
|
projectId: string,
|
|
128
149
|
accountId: string,
|
|
@@ -109,12 +109,12 @@ describe("backupSelfHosted", () => {
|
|
|
109
109
|
expect(out).toEqual({ ok: true, path: to });
|
|
110
110
|
expect(probe(r.calls.map((c) => c.cmd))).toEqual([
|
|
111
111
|
"exec -T postgres pg_isready -U caelo",
|
|
112
|
-
"stop caelo-admin caelo-gateway",
|
|
112
|
+
"stop caelo-admin caelo-gateway caelo-staging-gateway",
|
|
113
113
|
"exec -T postgres pg_dumpall -U caelo --globals-only",
|
|
114
114
|
"exec -T postgres pg_dump -U caelo -Fc cms_admin",
|
|
115
115
|
"exec -T postgres pg_dump -U caelo -Fc cms_public",
|
|
116
116
|
"run --rm --no-deps -T --entrypoint tar caelo-admin -C / -cf - srv/caelo data/media",
|
|
117
|
-
"up -d caelo-admin caelo-gateway",
|
|
117
|
+
"up -d caelo-admin caelo-gateway caelo-staging-gateway",
|
|
118
118
|
"exec -T caelo-admin bun -e <probe>",
|
|
119
119
|
"exec -T caelo-gateway bun -e <probe>",
|
|
120
120
|
]);
|
|
@@ -146,7 +146,9 @@ describe("backupSelfHosted", () => {
|
|
|
146
146
|
...quiet,
|
|
147
147
|
});
|
|
148
148
|
expect(out.ok).toBe(false);
|
|
149
|
-
expect(r.calls.map((c) => c.cmd)).toContain(
|
|
149
|
+
expect(r.calls.map((c) => c.cmd)).toContain(
|
|
150
|
+
"up -d caelo-admin caelo-gateway caelo-staging-gateway",
|
|
151
|
+
);
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
it("refuses when Postgres is not running and changes nothing", async () => {
|
|
@@ -194,7 +196,7 @@ describe("restoreSelfHosted", () => {
|
|
|
194
196
|
const out = await restoreSelfHosted(archive, restoreDeps(r));
|
|
195
197
|
expect(out.ok).toBe(true);
|
|
196
198
|
expect(probe(r.calls.map((c) => c.cmd))).toEqual([
|
|
197
|
-
"stop caelo-admin caelo-gateway",
|
|
199
|
+
"stop caelo-admin caelo-gateway caelo-staging-gateway",
|
|
198
200
|
"up -d --wait postgres",
|
|
199
201
|
"exec -T postgres psql -U caelo -d postgres -v ON_ERROR_STOP=1 -c DROP DATABASE IF EXISTS cms_admin WITH (FORCE) -c DROP DATABASE IF EXISTS cms_public WITH (FORCE)",
|
|
200
202
|
"<converge roles>",
|