@caelo-cms/provisioning 0.10.49 → 0.10.51
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/caddy/Caddyfile.production +4 -0
- package/caddy/Caddyfile.staging +4 -0
- package/dist/adapter.d.ts +4 -6
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-internal.d.ts +41 -0
- package/dist/admin-internal.d.ts.map +1 -0
- package/dist/admin-internal.js +169 -0
- package/dist/admin-internal.js.map +1 -0
- package/dist/agent-db.js +93 -20
- package/dist/aws-task-roles.d.ts +27 -0
- package/dist/aws-task-roles.d.ts.map +1 -0
- package/dist/aws-task-roles.js +35 -0
- package/dist/aws-task-roles.js.map +1 -0
- package/dist/caddy-redirects.d.ts +15 -0
- package/dist/caddy-redirects.d.ts.map +1 -0
- package/dist/caddy-redirects.js +65 -0
- package/dist/caddy-redirects.js.map +1 -0
- package/dist/caddy.d.ts +22 -2
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +66 -17
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +7 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +282 -255
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +54 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +88 -34
- package/dist/compose.js.map +1 -1
- package/dist/deployed-release.d.ts +3 -2
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +12 -5
- package/dist/deployed-release.js.map +1 -1
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +6 -3
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +2 -2
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +3 -3
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +12 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +27 -0
- package/dist/gcp-url-map.d.ts.map +1 -0
- package/dist/gcp-url-map.js +62 -0
- package/dist/gcp-url-map.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +20 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +44 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/owner-reset.d.ts +37 -0
- package/dist/owner-reset.d.ts.map +1 -0
- package/dist/owner-reset.js +80 -0
- package/dist/owner-reset.js.map +1 -0
- package/dist/public-role-isolation.d.ts +19 -0
- package/dist/public-role-isolation.d.ts.map +1 -0
- package/dist/public-role-isolation.js +69 -0
- package/dist/public-role-isolation.js.map +1 -0
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/self-hosted-backup.d.ts +86 -0
- package/dist/self-hosted-backup.d.ts.map +1 -0
- package/dist/self-hosted-backup.js +350 -0
- package/dist/self-hosted-backup.js.map +1 -0
- package/dist/self-hosted-release.d.ts +12 -3
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -4
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-start.d.ts +50 -0
- package/dist/self-hosted-start.d.ts.map +1 -0
- package/dist/self-hosted-start.js +59 -0
- package/dist/self-hosted-start.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +30 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +29 -7
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +9 -10
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +21 -10
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-admin.d.ts +12 -20
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js +7 -159
- package/dist/staging-admin.js.map +1 -1
- package/dist/tar-entries.d.ts +14 -0
- package/dist/tar-entries.d.ts.map +1 -0
- package/dist/tar-entries.js +104 -0
- package/dist/tar-entries.js.map +1 -0
- package/dist/upgrade/converge.d.ts +2 -2
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +44 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +2 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/url-map-redirects-access.d.ts +15 -0
- package/dist/url-map-redirects-access.d.ts.map +1 -0
- package/dist/url-map-redirects-access.js +81 -0
- package/dist/url-map-redirects-access.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.js +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +45 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +4 -6
- package/src/admin-internal.ts +212 -0
- package/src/admin-memory.test.ts +2 -2
- package/src/aws-task-roles.ts +58 -0
- package/src/caddy-redirects.docker.test.ts +255 -0
- package/src/caddy-redirects.test.ts +115 -0
- package/src/caddy-redirects.ts +68 -0
- package/src/caddy-validate.test.ts +119 -0
- package/src/caddy.test.ts +108 -0
- package/src/caddy.ts +89 -20
- package/src/checkpoint-secrets.test.ts +0 -1
- package/src/cli-routing.test.ts +12 -0
- package/src/cli-routing.ts +7 -0
- package/src/cli.ts +340 -288
- package/src/compose.ts +108 -36
- package/src/deployed-release.test.ts +22 -4
- package/src/deployed-release.ts +14 -7
- package/src/gateway-credentials.ts +6 -3
- package/src/gateway-env-isolation.test.ts +212 -0
- package/src/gcloud.ts +5 -3
- package/src/gcp-names.ts +23 -0
- package/src/gcp-url-map.test.ts +150 -0
- package/src/gcp-url-map.ts +81 -0
- package/src/index.test.ts +24 -152
- package/src/index.ts +0 -9
- package/src/infra-contract.test.ts +40 -0
- package/src/infra-contract.ts +47 -1
- package/src/owner-reset.test.ts +165 -0
- package/src/owner-reset.ts +106 -0
- package/src/public-role-isolation.test.ts +89 -0
- package/src/public-role-isolation.ts +83 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +12 -0
- package/src/release-manifest.ts +2 -1
- package/src/runtime-secrets.test.ts +25 -8
- package/src/self-hosted-backup.test.ts +344 -0
- package/src/self-hosted-backup.ts +480 -0
- package/src/self-hosted-compose.test.ts +88 -4
- package/src/self-hosted-release.ts +22 -7
- package/src/self-hosted-start.test.ts +68 -0
- package/src/self-hosted-start.ts +123 -0
- package/src/self-hosted-upgrade.ts +35 -8
- package/src/stack-contract.test.ts +11 -20
- package/src/stack-contract.ts +25 -16
- package/src/stack-converge-secrets.test.ts +45 -10
- package/src/stack-converge.test.ts +19 -10
- package/src/staging-admin.ts +18 -181
- package/src/tar-entries.test.ts +52 -0
- package/src/tar-entries.ts +116 -0
- package/src/upgrade/converge.ts +51 -4
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +22 -0
- package/src/upgrader-assess.ts +2 -1
- package/src/url-map-redirects-access.test.ts +88 -0
- package/src/url-map-redirects-access.ts +89 -0
- package/src/wizard.ts +3 -1
- package/src/wizards/gcp-cost.ts +1 -1
- package/src/wizards/gcp.ts +48 -0
- package/stacks/aws/README.md +1 -1
- package/stacks/aws/index.ts +89 -45
- package/stacks/azure/README.md +1 -1
- package/stacks/azure/index.ts +21 -14
- package/stacks/gcp/README.md +1 -1
- package/stacks/gcp/index.ts +26 -22
- package/stacks/gcp-firebase/index.ts +7 -3
- package/stacks/self-hosted/Pulumi.yaml +2 -2
- package/stacks/self-hosted/README.md +3 -3
- package/stacks/self-hosted/index.ts +45 -22
- package/dist/redirects-emit.d.ts +0 -52
- package/dist/redirects-emit.d.ts.map +0 -1
- package/dist/redirects-emit.js +0 -111
- package/dist/redirects-emit.js.map +0 -1
- package/src/redirects-emit.ts +0 -175
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import type { CommandResult, HostRunner } from "./self-hosted-release.js";
|
|
5
|
+
import { startSelfHosted } from "./self-hosted-start.js";
|
|
6
|
+
|
|
7
|
+
const DIR = "/srv/install/.caelo";
|
|
8
|
+
|
|
9
|
+
/** A runner that records every command (minus the `docker compose -f <file>` prefix). */
|
|
10
|
+
function fakeRunner(fail?: (cmd: string) => boolean): HostRunner & { calls: string[] } {
|
|
11
|
+
const calls: string[] = [];
|
|
12
|
+
return {
|
|
13
|
+
calls,
|
|
14
|
+
async run(argv): Promise<CommandResult> {
|
|
15
|
+
const cmd = argv.join(" ").replace(`docker compose -f ${DIR}/docker-compose.yml `, "");
|
|
16
|
+
calls.push(cmd);
|
|
17
|
+
return { exitCode: fail?.(cmd) ? 1 : 0, stdout: "", stderr: fail?.(cmd) ? "boom" : "" };
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MIGRATE =
|
|
23
|
+
"run --rm --no-deps -T caelo-admin bun --bun /app/packages/migrations/src/migrate.ts";
|
|
24
|
+
|
|
25
|
+
describe("startSelfHosted (#707 — one command ends with a serving install)", () => {
|
|
26
|
+
it("pulls, starts Postgres, migrates, starts everything, then health-checks both services", async () => {
|
|
27
|
+
const runner = fakeRunner();
|
|
28
|
+
const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
|
|
29
|
+
expect(r).toEqual({ ok: true });
|
|
30
|
+
expect(runner.calls.map((c) => c.replace(/ -e .*/, " -e <probe>"))).toEqual([
|
|
31
|
+
"pull",
|
|
32
|
+
"up -d --wait postgres",
|
|
33
|
+
`${MIGRATE} admin`,
|
|
34
|
+
`${MIGRATE} public`,
|
|
35
|
+
"up -d --remove-orphans",
|
|
36
|
+
"exec -T caelo-admin bun -e <probe>",
|
|
37
|
+
"exec -T caelo-gateway bun -e <probe>",
|
|
38
|
+
"exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile",
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("stops at a failed migration — nothing else is started", async () => {
|
|
43
|
+
const runner = fakeRunner((c) => c.endsWith("migrate.ts admin"));
|
|
44
|
+
const r = await startSelfHosted({ caeloDir: DIR, runner, log: () => {} });
|
|
45
|
+
expect(r.ok).toBe(false);
|
|
46
|
+
if (!r.ok) {
|
|
47
|
+
expect(r.stage).toBe("migrate");
|
|
48
|
+
expect(r.error).toContain("admin migrations failed");
|
|
49
|
+
}
|
|
50
|
+
expect(runner.calls).not.toContain("up -d --remove-orphans");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("reports an unhealthy admin with where to look", async () => {
|
|
54
|
+
const runner = fakeRunner((c) => c.startsWith("exec -T caelo-admin"));
|
|
55
|
+
const r = await startSelfHosted({
|
|
56
|
+
caeloDir: DIR,
|
|
57
|
+
runner,
|
|
58
|
+
log: () => {},
|
|
59
|
+
healthAttempts: 2,
|
|
60
|
+
sleep: async () => {},
|
|
61
|
+
});
|
|
62
|
+
expect(r.ok).toBe(false);
|
|
63
|
+
if (!r.ok) {
|
|
64
|
+
expect(r.stage).toBe("health");
|
|
65
|
+
expect(r.error).toContain("logs caelo-admin");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Start (or converge) a self-hosted Compose install — what `init` and `up`
|
|
5
|
+
* run after writing `.caelo/`, so one command ends with a serving install
|
|
6
|
+
* (CLAUDE.md §11.C, issue #707) instead of printing steps to copy.
|
|
7
|
+
*
|
|
8
|
+
* Order, and why:
|
|
9
|
+
* 1. pull every image, with the progress on the terminal (the admin image
|
|
10
|
+
* is large; a silent multi-minute step reads as a hang);
|
|
11
|
+
* 2. start Postgres and wait for its health check — on a fresh volume its
|
|
12
|
+
* first start runs bootstrap.sh (roles + databases);
|
|
13
|
+
* 3. apply the migrations with the install's admin image;
|
|
14
|
+
* 4. start everything (`--remove-orphans` drops services an older Compose
|
|
15
|
+
* file had, e.g. the pgBackRest sidecar);
|
|
16
|
+
* 5. health-check the admin (`/_caelo/health` proves the DB pool and its
|
|
17
|
+
* role) and the gateway;
|
|
18
|
+
* 6. reload Caddy — Compose does not restart a running Caddy when only
|
|
19
|
+
* its bind-mounted Caddyfile changed, so a re-run `up` would otherwise
|
|
20
|
+
* keep serving the old vhosts.
|
|
21
|
+
* Every step converges, so a re-run after a failure picks up where it
|
|
22
|
+
* stopped.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { HostRunner } from "./self-hosted-release.js";
|
|
26
|
+
import { compose, runMigrations, waitHealthy } from "./self-hosted-upgrade.js";
|
|
27
|
+
|
|
28
|
+
export interface StartDeps {
|
|
29
|
+
readonly caeloDir: string;
|
|
30
|
+
readonly runner: HostRunner;
|
|
31
|
+
readonly log: (message: string) => void;
|
|
32
|
+
/** Health probe attempts per service and the pause between them. */
|
|
33
|
+
readonly healthAttempts?: number;
|
|
34
|
+
readonly healthIntervalMs?: number;
|
|
35
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type StartResult =
|
|
39
|
+
| { readonly ok: true }
|
|
40
|
+
| { readonly ok: false; readonly stage: StartStage; readonly error: string };
|
|
41
|
+
|
|
42
|
+
type StartStage = "pull" | "postgres" | "migrate" | "start" | "health" | "caddy";
|
|
43
|
+
|
|
44
|
+
/** Bring the install's Compose stack up and prove it serves. Never throws for an expected failure. */
|
|
45
|
+
export async function startSelfHosted(deps: StartDeps): Promise<StartResult> {
|
|
46
|
+
const { caeloDir, runner, log } = deps;
|
|
47
|
+
log("Pulling the images …");
|
|
48
|
+
const pull = await runner.run(compose(caeloDir, "pull"), { inherit: true });
|
|
49
|
+
if (pull.exitCode !== 0) {
|
|
50
|
+
return fail("pull", "docker compose pull failed (see above). Check the network and re-run.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log("Starting Postgres …");
|
|
54
|
+
const pg = await runner.run(compose(caeloDir, "up", "-d", "--wait", "postgres"), {
|
|
55
|
+
inherit: true,
|
|
56
|
+
});
|
|
57
|
+
if (pg.exitCode !== 0) {
|
|
58
|
+
return fail(
|
|
59
|
+
"postgres",
|
|
60
|
+
`Postgres did not become healthy (see above; \`docker compose -f ${caeloDir}/docker-compose.yml logs postgres\`).`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
log("Applying the database migrations …");
|
|
65
|
+
const migrated = await runMigrations(caeloDir, runner);
|
|
66
|
+
if (!migrated.ok) return fail("migrate", migrated.error);
|
|
67
|
+
|
|
68
|
+
log("Starting Caddy, the admin and the gateway …");
|
|
69
|
+
const up = await runner.run(compose(caeloDir, "up", "-d", "--remove-orphans"), {
|
|
70
|
+
inherit: true,
|
|
71
|
+
});
|
|
72
|
+
if (up.exitCode !== 0) return fail("start", "docker compose up failed (see above).");
|
|
73
|
+
|
|
74
|
+
for (const service of ["admin", "gateway"] as const) {
|
|
75
|
+
log(`Waiting for the ${service} to answer its health check …`);
|
|
76
|
+
const healthy = await waitHealthy(service, caeloDir, {
|
|
77
|
+
runner,
|
|
78
|
+
...(deps.sleep ? { sleep: deps.sleep } : {}),
|
|
79
|
+
...(deps.healthAttempts !== undefined ? { healthAttempts: deps.healthAttempts } : {}),
|
|
80
|
+
...(deps.healthIntervalMs !== undefined ? { healthIntervalMs: deps.healthIntervalMs } : {}),
|
|
81
|
+
});
|
|
82
|
+
if (!healthy.ok) {
|
|
83
|
+
return fail(
|
|
84
|
+
"health",
|
|
85
|
+
`The ${service} is not healthy: ${healthy.error}. See \`docker compose -f ${caeloDir}/docker-compose.yml logs caelo-${service}\`.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
log("Reloading Caddy …");
|
|
90
|
+
const reloaded = await reloadCaddy(caeloDir, runner);
|
|
91
|
+
if (!reloaded.ok) return fail("caddy", reloaded.error);
|
|
92
|
+
return { ok: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `caddy reload` in the running Caddy container with the current Caddyfile. */
|
|
96
|
+
export async function reloadCaddy(
|
|
97
|
+
caeloDir: string,
|
|
98
|
+
runner: HostRunner,
|
|
99
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
100
|
+
const r = await runner.run(
|
|
101
|
+
compose(
|
|
102
|
+
caeloDir,
|
|
103
|
+
"exec",
|
|
104
|
+
"-T",
|
|
105
|
+
"caddy",
|
|
106
|
+
"caddy",
|
|
107
|
+
"reload",
|
|
108
|
+
"--config",
|
|
109
|
+
"/etc/caddy/Caddyfile",
|
|
110
|
+
"--adapter",
|
|
111
|
+
"caddyfile",
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
if (r.exitCode === 0) return { ok: true };
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `caddy reload failed: ${r.stderr.trim().split("\n").slice(-3).join("\n")}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fail(stage: StartStage, error: string): StartResult {
|
|
122
|
+
return { ok: false, stage, error };
|
|
123
|
+
}
|
|
@@ -196,7 +196,7 @@ export function composePath(caeloDir: string): string {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/** `docker compose -f <file>` — every Compose call targets the install's file explicitly. */
|
|
199
|
-
function compose(caeloDir: string, ...args: string[]): string[] {
|
|
199
|
+
export function compose(caeloDir: string, ...args: string[]): string[] {
|
|
200
200
|
return ["docker", "compose", "-f", composePath(caeloDir), ...args];
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -415,12 +415,35 @@ export async function backupDatabases(
|
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
417
|
const dir = join(caeloDir, "backups", `pre-upgrade-${at.toISOString().replace(/[:.]/g, "-")}`);
|
|
418
|
+
const dumped = await dumpDatabases(caeloDir, runner, dir);
|
|
419
|
+
if (!dumped.ok) return { ok: false, error: `${dumped.error} The upgrade did not start.` };
|
|
420
|
+
return { ok: true, dir };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The files {@link dumpDatabases} writes, and what each holds. */
|
|
424
|
+
export const DATABASE_DUMPS = {
|
|
425
|
+
/** The cluster's roles (reference only — restores re-create them from config.json). */
|
|
426
|
+
globals: "globals.sql",
|
|
427
|
+
cms_admin: "cms_admin.dump",
|
|
428
|
+
cms_public: "cms_public.dump",
|
|
429
|
+
} as const;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* pg_dump both databases (custom format) plus the cluster's roles into
|
|
433
|
+
* `dir` (created mode 700) from the running Postgres container. Every dump
|
|
434
|
+
* must exit 0 and be non-empty.
|
|
435
|
+
*/
|
|
436
|
+
export async function dumpDatabases(
|
|
437
|
+
caeloDir: string,
|
|
438
|
+
runner: HostRunner,
|
|
439
|
+
dir: string,
|
|
440
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
418
441
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
419
442
|
chmodSync(dir, 0o700);
|
|
420
443
|
const dumps: [string, string[]][] = [
|
|
421
|
-
[
|
|
422
|
-
[
|
|
423
|
-
[
|
|
444
|
+
[DATABASE_DUMPS.globals, ["pg_dumpall", "-U", "caelo", "--globals-only"]],
|
|
445
|
+
[DATABASE_DUMPS.cms_admin, ["pg_dump", "-U", "caelo", "-Fc", "cms_admin"]],
|
|
446
|
+
[DATABASE_DUMPS.cms_public, ["pg_dump", "-U", "caelo", "-Fc", "cms_public"]],
|
|
424
447
|
];
|
|
425
448
|
for (const [name, cmd] of dumps) {
|
|
426
449
|
const out = join(dir, name);
|
|
@@ -430,12 +453,12 @@ export async function backupDatabases(
|
|
|
430
453
|
if (r.exitCode !== 0 || fileSize(out) === 0) {
|
|
431
454
|
return {
|
|
432
455
|
ok: false,
|
|
433
|
-
error: `Backup failed (${cmd.join(" ")} → ${out}): ${tail(r.stderr) || "empty dump"}
|
|
456
|
+
error: `Backup failed (${cmd.join(" ")} → ${out}): ${tail(r.stderr) || "empty dump"}.`,
|
|
434
457
|
};
|
|
435
458
|
}
|
|
436
459
|
chmodSync(out, 0o600);
|
|
437
460
|
}
|
|
438
|
-
return { ok: true
|
|
461
|
+
return { ok: true };
|
|
439
462
|
}
|
|
440
463
|
|
|
441
464
|
/**
|
|
@@ -483,10 +506,14 @@ const HEALTH_PROBE: Record<"admin" | "gateway", string> = {
|
|
|
483
506
|
'const r=await fetch("http://127.0.0.1:"+(process.env.PORT||process.env.GATEWAY_PORT||"8090")+"/healthz").catch(()=>null);process.exit(r&&r.ok?0:1)',
|
|
484
507
|
};
|
|
485
508
|
|
|
486
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Probe `service` inside its container until it answers healthy, or give up
|
|
511
|
+
* after `healthAttempts` tries (default 45, 2 s apart).
|
|
512
|
+
*/
|
|
513
|
+
export async function waitHealthy(
|
|
487
514
|
service: "admin" | "gateway",
|
|
488
515
|
caeloDir: string,
|
|
489
|
-
deps: SelfHostedUpgradeDeps,
|
|
516
|
+
deps: Pick<SelfHostedUpgradeDeps, "runner" | "sleep" | "healthAttempts" | "healthIntervalMs">,
|
|
490
517
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
491
518
|
const attempts = deps.healthAttempts ?? 45;
|
|
492
519
|
const interval = deps.healthIntervalMs ?? 2000;
|
|
@@ -49,12 +49,11 @@ const ADMIN_SECRETS = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
describe("databaseUrls", () => {
|
|
52
|
-
it("builds password-less URLs for the
|
|
52
|
+
it("builds password-less URLs for the three role/database pairs", () => {
|
|
53
53
|
expect(databaseUrls("10.20.0.3")).toEqual({
|
|
54
54
|
admin: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
|
|
55
55
|
publicAdmin: "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
|
|
56
56
|
public: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
|
|
57
|
-
gateway: "postgresql://gateway_role@10.20.0.3:5432/cms_admin?sslmode=require",
|
|
58
57
|
});
|
|
59
58
|
});
|
|
60
59
|
});
|
|
@@ -154,15 +153,13 @@ describe.each([
|
|
|
154
153
|
describe("gatewayEnvContract", () => {
|
|
155
154
|
const gateway = gatewayEnvContract({ ...base, provider: "gcp" });
|
|
156
155
|
|
|
157
|
-
it("carries its
|
|
156
|
+
it("carries its one own login and none of the admin's database or app secrets (#635)", () => {
|
|
158
157
|
expect(asRecord(gateway)).toEqual({
|
|
159
158
|
CAELO_PROVIDER: "gcp",
|
|
160
159
|
CAELO_ENV: "production",
|
|
161
160
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
162
161
|
PUBLIC_DATABASE_URL: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
|
|
163
|
-
GATEWAY_DATABASE_URL: "postgresql://gateway_role@10.20.0.3:5432/cms_admin?sslmode=require",
|
|
164
162
|
PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
|
|
165
|
-
GATEWAY_DATABASE_PASSWORD: "secret:caelo-production-gateway-role-password:latest",
|
|
166
163
|
CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
|
|
167
164
|
});
|
|
168
165
|
});
|
|
@@ -188,11 +185,7 @@ describe("gatewayEnvContract", () => {
|
|
|
188
185
|
});
|
|
189
186
|
|
|
190
187
|
it("the gateway's run SA reads no admin-only secret (admin password, KEK, internal, tool approval)", () => {
|
|
191
|
-
expect(serviceSecrets("gateway")).toEqual([
|
|
192
|
-
"gateway-role-password",
|
|
193
|
-
"public-role-password",
|
|
194
|
-
"staging-gate-secret",
|
|
195
|
-
]);
|
|
188
|
+
expect(serviceSecrets("gateway")).toEqual(["public-role-password", "staging-gate-secret"]);
|
|
196
189
|
for (const v of gateway) {
|
|
197
190
|
expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
|
|
198
191
|
}
|
|
@@ -280,11 +273,6 @@ describe("runtime secrets", () => {
|
|
|
280
273
|
secret: "staging-gate-secret",
|
|
281
274
|
stackResource: "staging-gate-secret-binding",
|
|
282
275
|
},
|
|
283
|
-
{
|
|
284
|
-
service: "gateway",
|
|
285
|
-
secret: "gateway-role-password",
|
|
286
|
-
stackResource: "gateway-gateway-role-password-binding",
|
|
287
|
-
},
|
|
288
276
|
{
|
|
289
277
|
service: "gateway",
|
|
290
278
|
secret: "public-role-password",
|
|
@@ -299,7 +287,11 @@ describe("runtime secrets", () => {
|
|
|
299
287
|
});
|
|
300
288
|
|
|
301
289
|
it("every CLI-generated secret is read by some service", () => {
|
|
302
|
-
|
|
290
|
+
// gateway-role-password is read by no service since #635: it only keeps
|
|
291
|
+
// the legacy gateway_role's password set until the contract release
|
|
292
|
+
// drops the role (the previous release's gateway logs in with it while
|
|
293
|
+
// an upgrade rolls).
|
|
294
|
+
for (const s of CLI_GENERATED_SECRETS.filter((x) => x !== "gateway-role-password")) {
|
|
303
295
|
expect([...serviceSecrets("admin"), ...serviceSecrets("gateway")]).toContain(s);
|
|
304
296
|
}
|
|
305
297
|
});
|
|
@@ -311,10 +303,10 @@ describe("self-hosted compose", () => {
|
|
|
311
303
|
domain: "acme.com",
|
|
312
304
|
postgresPassword: "p",
|
|
313
305
|
rolePasswords: { admin: "a", public: "b", gateway: "g" },
|
|
314
|
-
minioRootUser: "u",
|
|
315
|
-
minioRootPassword: "m",
|
|
316
306
|
caeloSecretKek: "k",
|
|
317
307
|
diskSize: "10G",
|
|
308
|
+
ports: { http: 80, https: 443 },
|
|
309
|
+
project: "caelo-example-com",
|
|
318
310
|
images: {
|
|
319
311
|
mode: "release" as const,
|
|
320
312
|
admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
|
|
@@ -351,13 +343,12 @@ describe("stackIamInvariants", () => {
|
|
|
351
343
|
}
|
|
352
344
|
});
|
|
353
345
|
|
|
354
|
-
it("grants the gateway SA only its
|
|
346
|
+
it("grants the gateway SA only its own role password, the staging gate key + telemetry roles", () => {
|
|
355
347
|
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
356
348
|
const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
|
|
357
349
|
expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
|
|
358
350
|
'roles/logging.logWriter {"kind":"project"}',
|
|
359
351
|
'roles/monitoring.metricWriter {"kind":"project"}',
|
|
360
|
-
'roles/secretmanager.secretAccessor {"kind":"secret","name":"gateway-role-password"}',
|
|
361
352
|
'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
|
|
362
353
|
'roles/secretmanager.secretAccessor {"kind":"secret","name":"staging-gate-secret"}',
|
|
363
354
|
]);
|
package/src/stack-contract.ts
CHANGED
|
@@ -86,14 +86,12 @@ export interface DatabaseUrls<V = string> {
|
|
|
86
86
|
readonly admin: V;
|
|
87
87
|
/** admin_role on cms_public — the admin's second pool (DDL, migrations). */
|
|
88
88
|
readonly publicAdmin: V;
|
|
89
|
-
/** public_role on cms_public — the gateway's visitor-write pool. */
|
|
90
|
-
readonly public: V;
|
|
91
89
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
90
|
+
* public_role on cms_public — the gateway's only pool. The gateway has no
|
|
91
|
+
* cms_admin login (#635, CLAUDE.md §2): what it needs from authoring
|
|
92
|
+
* state the admin publishes into cms_public.
|
|
95
93
|
*/
|
|
96
|
-
readonly
|
|
94
|
+
readonly public: V;
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/** Port Cloud SQL Postgres listens on (private IP). */
|
|
@@ -111,7 +109,6 @@ export function databaseUrls(host: string): DatabaseUrls {
|
|
|
111
109
|
admin: url("admin_role", "cms_admin"),
|
|
112
110
|
publicAdmin: url("admin_role", "cms_public"),
|
|
113
111
|
public: url("public_role", "cms_public"),
|
|
114
|
-
gateway: url("gateway_role", "cms_admin"),
|
|
115
112
|
};
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -251,8 +248,8 @@ export type DatabaseRole = "admin_role" | "public_role" | "gateway_role";
|
|
|
251
248
|
|
|
252
249
|
/**
|
|
253
250
|
* Each database role's password secret — one per role (#613), so the
|
|
254
|
-
* gateway's SA, which reads only the public_role
|
|
255
|
-
*
|
|
251
|
+
* gateway's SA, which reads only the public_role password, can never read
|
|
252
|
+
* admin_role's (or, since #635, gateway_role's). `postgres-password` keeps its
|
|
256
253
|
* name: it has always been admin_role's (and, before #613, shared with
|
|
257
254
|
* public_role). `rotate-secret` changes exactly the role(s) listed here.
|
|
258
255
|
*/
|
|
@@ -306,9 +303,8 @@ export const SERVICE_SECRET_ENV: Readonly<
|
|
|
306
303
|
CAELO_STAGING_GATE_SECRET: "staging-gate-secret",
|
|
307
304
|
},
|
|
308
305
|
gateway: {
|
|
309
|
-
// CLAUDE.md §2:
|
|
310
|
-
//
|
|
311
|
-
GATEWAY_DATABASE_PASSWORD: "gateway-role-password",
|
|
306
|
+
// CLAUDE.md §2: no cms_admin credential of any role (#613, #635).
|
|
307
|
+
// public_role on cms_public is the gateway's only login.
|
|
312
308
|
PUBLIC_DATABASE_PASSWORD: "public-role-password",
|
|
313
309
|
// #650 — verifies staging-host requests (api-gateway staging-proxy.ts).
|
|
314
310
|
CAELO_STAGING_GATE_SECRET: "staging-gate-secret",
|
|
@@ -323,12 +319,20 @@ export function serviceSecrets(service: CloudRunSlug): RuntimeSecret[] {
|
|
|
323
319
|
/**
|
|
324
320
|
* Env vars an older stack set that the contract no longer gives a service;
|
|
325
321
|
* upgrade removes them. The gateway never used the KEK, and its run SA
|
|
326
|
-
* cannot read it. Its admin_role pool went with #613
|
|
327
|
-
*
|
|
322
|
+
* cannot read it. Its admin_role pool went with #613 and its gateway_role
|
|
323
|
+
* pool with #635 — the gateway refuses to boot while any cms_admin
|
|
324
|
+
* credential is in its env (apps/api-gateway `adminDatabaseAccessInEnv`).
|
|
328
325
|
*/
|
|
329
326
|
export const RETIRED_SERVICE_ENV: Readonly<Record<CloudRunSlug, readonly string[]>> = {
|
|
330
327
|
admin: [],
|
|
331
|
-
gateway: [
|
|
328
|
+
gateway: [
|
|
329
|
+
"CAELO_SECRET_KEK",
|
|
330
|
+
"ADMIN_DATABASE_URL",
|
|
331
|
+
"ADMIN_DATABASE_PASSWORD",
|
|
332
|
+
// #635 — its gateway_role login on cms_admin is gone too.
|
|
333
|
+
"GATEWAY_DATABASE_URL",
|
|
334
|
+
"GATEWAY_DATABASE_PASSWORD",
|
|
335
|
+
],
|
|
332
336
|
};
|
|
333
337
|
|
|
334
338
|
function secretEnv(service: CloudRunSlug, env: string): CloudRunEnvVar[] {
|
|
@@ -362,7 +366,6 @@ export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvV
|
|
|
362
366
|
return [
|
|
363
367
|
...commonEnv(inputs),
|
|
364
368
|
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.public },
|
|
365
|
-
{ name: "GATEWAY_DATABASE_URL", value: inputs.databaseUrls.gateway },
|
|
366
369
|
...secretEnv("gateway", inputs.env),
|
|
367
370
|
];
|
|
368
371
|
}
|
|
@@ -679,6 +682,12 @@ export const RETIRED_IAM_BINDINGS: readonly RetiredIamBinding[] = [
|
|
|
679
682
|
target: { kind: "secret", name: "postgres-password" },
|
|
680
683
|
why: "the gateway no longer logs in as admin_role and must not be able to read its password (CLAUDE.md §2)",
|
|
681
684
|
},
|
|
685
|
+
{
|
|
686
|
+
role: SECRET_ACCESSOR,
|
|
687
|
+
member: "gateway-sa",
|
|
688
|
+
target: { kind: "secret", name: "gateway-role-password" },
|
|
689
|
+
why: "the gateway has no cms_admin login at all and must not be able to read gateway_role's password (CLAUDE.md §2, #635)",
|
|
690
|
+
},
|
|
682
691
|
];
|
|
683
692
|
|
|
684
693
|
/** Every IAM binding upgrade ensures on an install of `provider`. */
|
|
@@ -38,12 +38,14 @@ const SECRET_VALUES: Record<string, string> = {
|
|
|
38
38
|
"caelo-production-gateway-role-password": "g".repeat(64),
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
/** Env vars that would hand the gateway
|
|
41
|
+
/** Env vars that would hand the gateway a cms_admin credential (a subset of what apps/api-gateway adminDatabaseAccessInEnv refuses). */
|
|
42
42
|
const ADMIN_CREDENTIAL_ENV = [
|
|
43
43
|
"ADMIN_DATABASE_URL",
|
|
44
44
|
"ADMIN_DATABASE_PASSWORD",
|
|
45
45
|
"PUBLIC_ADMIN_DATABASE_URL",
|
|
46
46
|
"PUBLIC_ADMIN_DATABASE_PASSWORD",
|
|
47
|
+
"GATEWAY_DATABASE_URL",
|
|
48
|
+
"GATEWAY_DATABASE_PASSWORD",
|
|
47
49
|
];
|
|
48
50
|
|
|
49
51
|
/** Split a gcloud list flag value, honouring the `^<delim>^` escape. */
|
|
@@ -122,16 +124,15 @@ function credentials(url: string | undefined): string {
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
|
-
* The gateway's process env after the upgrade:
|
|
126
|
-
*
|
|
127
|
-
*
|
|
127
|
+
* The gateway's process env after the upgrade: public_role on cms_public
|
|
128
|
+
* with its own secret, and no cms_admin credential of any role at all
|
|
129
|
+
* (#613, #635 — the gateway refuses to boot with one).
|
|
128
130
|
*/
|
|
129
131
|
function expectGatewayOnItsOwnLogins(env: Record<string, string>): void {
|
|
130
132
|
for (const name of ADMIN_CREDENTIAL_ENV) expect(Object.keys(env)).not.toContain(name);
|
|
131
133
|
expect(Object.values(env).join("\n")).not.toContain(PW);
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
);
|
|
134
|
+
expect(Object.values(env).join("\n")).not.toContain("g".repeat(64));
|
|
135
|
+
expect(Object.values(env).join("\n")).not.toContain("cms_admin");
|
|
135
136
|
expect(credentials(databaseUrlFromEnv(["PUBLIC_DATABASE_URL"], env))).toBe(
|
|
136
137
|
`public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=require`,
|
|
137
138
|
);
|
|
@@ -331,7 +332,7 @@ function v01035Install() {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", () => {
|
|
334
|
-
it("drops the admin_role credential and adds the gateway's own
|
|
335
|
+
it("drops the admin_role credential and adds the gateway's own login in one revision", () => {
|
|
335
336
|
const services = v01035Install();
|
|
336
337
|
const plan = upgrade(services);
|
|
337
338
|
expect(services.gateway.revisions).toHaveLength(2);
|
|
@@ -343,8 +344,6 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
343
344
|
"ADMIN_DATABASE_URL",
|
|
344
345
|
// #650 — the staging gate key arrives in the same roll.
|
|
345
346
|
"CAELO_STAGING_GATE_SECRET",
|
|
346
|
-
"GATEWAY_DATABASE_PASSWORD",
|
|
347
|
-
"GATEWAY_DATABASE_URL",
|
|
348
347
|
"PUBLIC_DATABASE_PASSWORD",
|
|
349
348
|
]);
|
|
350
349
|
});
|
|
@@ -374,6 +373,42 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
374
373
|
});
|
|
375
374
|
});
|
|
376
375
|
|
|
376
|
+
/**
|
|
377
|
+
* A v0.10.50 install (#613, before #635): the gateway on gateway_role's
|
|
378
|
+
* cms_admin pool next to public_role's.
|
|
379
|
+
*/
|
|
380
|
+
function v01050Install() {
|
|
381
|
+
const services = v01035Install();
|
|
382
|
+
upgrade(services);
|
|
383
|
+
const env = new Map(services.gateway.env);
|
|
384
|
+
env.set("GATEWAY_DATABASE_URL", {
|
|
385
|
+
kind: "value",
|
|
386
|
+
value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=require`,
|
|
387
|
+
});
|
|
388
|
+
env.set("GATEWAY_DATABASE_PASSWORD", {
|
|
389
|
+
kind: "secret",
|
|
390
|
+
secret: "caelo-production-gateway-role-password",
|
|
391
|
+
version: "latest",
|
|
392
|
+
});
|
|
393
|
+
services.gateway = new FakeService(env, SERVICE_ACCOUNTS.gateway);
|
|
394
|
+
return services;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
describe("upgrading a v0.10.50 install: the gateway leaves cms_admin entirely (#635)", () => {
|
|
398
|
+
it("removes the gateway_role login in one revision and keeps the public pool", () => {
|
|
399
|
+
const services = v01050Install();
|
|
400
|
+
const plan = upgrade(services);
|
|
401
|
+
expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
|
|
402
|
+
"GATEWAY_DATABASE_PASSWORD",
|
|
403
|
+
"GATEWAY_DATABASE_URL",
|
|
404
|
+
]);
|
|
405
|
+
expectGatewayOnItsOwnLogins(containerEnv(services.gateway.env));
|
|
406
|
+
for (const revision of services.gateway.revisions) {
|
|
407
|
+
expect(revision.has("PUBLIC_DATABASE_URL")).toBe(true);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
377
412
|
describe("plain → Secret Manager migration of an existing var", () => {
|
|
378
413
|
it("never leaves a revision without the var", () => {
|
|
379
414
|
// An operator set CAELO_INTERNAL_SECRET by hand as a plain value.
|
|
@@ -268,12 +268,7 @@ const CONVERGED_GATEWAY_DB = [
|
|
|
268
268
|
name: "PUBLIC_DATABASE_URL",
|
|
269
269
|
value: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
|
|
270
270
|
},
|
|
271
|
-
{
|
|
272
|
-
name: "GATEWAY_DATABASE_URL",
|
|
273
|
-
value: "postgresql://gateway_role@10.20.0.3:5432/cms_admin?sslmode=require",
|
|
274
|
-
},
|
|
275
271
|
secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
|
|
276
|
-
secretRef("GATEWAY_DATABASE_PASSWORD", "gateway-role-password"),
|
|
277
272
|
];
|
|
278
273
|
|
|
279
274
|
describe("planContractEnv", () => {
|
|
@@ -776,10 +771,10 @@ describe("ensureStackInvariants", () => {
|
|
|
776
771
|
const gatewayGrants = calls.filter(
|
|
777
772
|
(c) => c.startsWith("secrets add-iam-policy-binding") && c.includes(GW_SA),
|
|
778
773
|
);
|
|
779
|
-
// #613 — never admin_role's postgres-password
|
|
780
|
-
// gate key is the one secret the
|
|
774
|
+
// #613 — never admin_role's postgres-password; #635 — nor
|
|
775
|
+
// gateway_role's. #650 — the staging gate key is the one secret the
|
|
776
|
+
// gateway shares with the admin.
|
|
781
777
|
expect(gatewayGrants).toEqual([
|
|
782
|
-
`secrets add-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
783
778
|
`secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
784
779
|
`secrets add-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
785
780
|
]);
|
|
@@ -802,7 +797,8 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
802
797
|
],
|
|
803
798
|
});
|
|
804
799
|
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
805
|
-
|
|
800
|
+
// postgres-password removed; gateway-role-password (#635) already clean.
|
|
801
|
+
expect(outcomes.map((o) => o.status)).toEqual(["removed", "absent"]);
|
|
806
802
|
expect(calls).toContain(REMOVE);
|
|
807
803
|
// The admin's own binding on the same secret is never touched.
|
|
808
804
|
expect(calls.some((c) => c.includes("remove") && c.includes(RUN_SA))).toBe(false);
|
|
@@ -815,10 +811,23 @@ describe("removeRetiredIamBindings (#613)", () => {
|
|
|
815
811
|
],
|
|
816
812
|
});
|
|
817
813
|
const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
|
|
818
|
-
expect(outcomes.map((o) => o.status)).toEqual(["absent"]);
|
|
814
|
+
expect(outcomes.map((o) => o.status)).toEqual(["absent", "absent"]);
|
|
819
815
|
expect(calls.filter((c) => c.includes("remove-iam-policy-binding"))).toEqual([]);
|
|
820
816
|
});
|
|
821
817
|
|
|
818
|
+
it("takes the gateway SA off gateway_role's password (#635)", async () => {
|
|
819
|
+
const { run, calls } = fakeGcloud({
|
|
820
|
+
"secrets get-iam-policy caelo-production-gateway-role-password": [
|
|
821
|
+
policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA] }]),
|
|
822
|
+
],
|
|
823
|
+
});
|
|
824
|
+
const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
|
|
825
|
+
expect(outcomes.map((o) => o.status)).toEqual(["absent", "removed"]);
|
|
826
|
+
expect(calls).toContain(
|
|
827
|
+
`secrets remove-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
828
|
+
);
|
|
829
|
+
});
|
|
830
|
+
|
|
822
831
|
it("reports a failed removal instead of throwing", async () => {
|
|
823
832
|
const { run } = fakeGcloud({
|
|
824
833
|
"secrets get-iam-policy caelo-production-postgres-password": [
|