@caelo-cms/provisioning 0.10.47 → 0.10.48
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/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +119 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +10 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +10 -2
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +19 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +4 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +1 -1
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +223 -59
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The gcloud runners against a fake `gcloud` on PATH: they must never leave
|
|
5
|
+
* gcloud able to wait on a prompt nobody sees (regression: `admin-domain
|
|
6
|
+
* enable` hung on gcloud's hidden "install the beta component? (Y/n)").
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
10
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { gcloud, gcloudEnv } from "./gcloud.js";
|
|
14
|
+
import { runCli } from "./regions.js";
|
|
15
|
+
|
|
16
|
+
// Echoes the prompt switch, then waits for one line on stdin, like gcloud's
|
|
17
|
+
// "(Y/n)" prompt does. With an open, never-ended stdin this blocks forever.
|
|
18
|
+
const FAKE_GCLOUD = `#!/bin/sh
|
|
19
|
+
echo "prompts=$CLOUDSDK_CORE_DISABLE_PROMPTS"
|
|
20
|
+
read -r answer
|
|
21
|
+
echo "answer=$answer"
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
let dir = "";
|
|
25
|
+
const originalPath = process.env.PATH;
|
|
26
|
+
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
dir = mkdtempSync(join(tmpdir(), "caelo-fake-gcloud-"));
|
|
29
|
+
writeFileSync(join(dir, "gcloud"), FAKE_GCLOUD);
|
|
30
|
+
chmodSync(join(dir, "gcloud"), 0o755);
|
|
31
|
+
process.env.PATH = `${dir}:${originalPath ?? ""}`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
process.env.PATH = originalPath;
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("gcloud runners never block on a hidden prompt", () => {
|
|
40
|
+
it("gcloud() disables prompts and closes stdin", async () => {
|
|
41
|
+
const r = await gcloud(["beta", "run", "domain-mappings", "describe"]);
|
|
42
|
+
expect(r.stdout).toBe("prompts=1\nanswer=\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("gcloud() still feeds stdin when the caller passes it", async () => {
|
|
46
|
+
const r = await gcloud(["secrets", "versions", "add"], { stdin: "payload\n" });
|
|
47
|
+
expect(r.stdout).toBe("prompts=1\nanswer=payload\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("runCli() disables gcloud prompts and closes stdin", async () => {
|
|
51
|
+
const r = await runCli("gcloud", ["config", "get-value", "run/region"]);
|
|
52
|
+
expect(r.stdout).toBe("prompts=1\nanswer=\n");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("gcloudEnv() keeps the caller's environment", () => {
|
|
56
|
+
expect(gcloudEnv({ HOME: "/home/x" })).toEqual({
|
|
57
|
+
HOME: "/home/x",
|
|
58
|
+
CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { type GcloudResult, grantProvisionerRoles, PROVISIONER_ROLE_LIST } from "./gcloud.js";
|
|
5
|
+
|
|
6
|
+
const ok: GcloudResult = { ok: true, stdout: "", stderr: "", exitCode: 0 };
|
|
7
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
8
|
+
|
|
9
|
+
const SA_NOT_YET_VISIBLE =
|
|
10
|
+
"ERROR: (gcloud.projects.add-iam-policy-binding) INVALID_ARGUMENT: Service account caelo-provisioner@p.iam.gserviceaccount.com does not exist.";
|
|
11
|
+
const PERMISSION_DENIED =
|
|
12
|
+
"ERROR: (gcloud.projects.add-iam-policy-binding) PERMISSION_DENIED: Policy update access denied.";
|
|
13
|
+
|
|
14
|
+
/** Fake gcloud: answers each call for `role` from its queue, `ok` once the queue is empty. */
|
|
15
|
+
function fakeGcloud(responses: Record<string, GcloudResult[]>) {
|
|
16
|
+
const calls: string[] = [];
|
|
17
|
+
const run = async (args: string[]): Promise<GcloudResult> => {
|
|
18
|
+
const role = args[args.indexOf("--role") + 1] as string;
|
|
19
|
+
calls.push(role);
|
|
20
|
+
return responses[role]?.shift() ?? ok;
|
|
21
|
+
};
|
|
22
|
+
return { run, calls };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const noSleep = async () => {};
|
|
26
|
+
|
|
27
|
+
describe("grantProvisionerRoles", () => {
|
|
28
|
+
it("binds only the given roles when asked (#696 install-job extras)", async () => {
|
|
29
|
+
const { run, calls } = fakeGcloud({});
|
|
30
|
+
const roles = ["roles/iam.roleAdmin", "roles/cloudscheduler.admin"];
|
|
31
|
+
const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep, roles });
|
|
32
|
+
expect(result).toEqual({ granted: 2, failed: [] });
|
|
33
|
+
expect(calls).toEqual(roles);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("retries a freshly created SA that IAM does not see yet (regression: first role failed right after SA create)", async () => {
|
|
37
|
+
const first = PROVISIONER_ROLE_LIST[0] as string;
|
|
38
|
+
const { run, calls } = fakeGcloud({
|
|
39
|
+
[first]: [fail(SA_NOT_YET_VISIBLE), fail(SA_NOT_YET_VISIBLE)],
|
|
40
|
+
});
|
|
41
|
+
const slept: number[] = [];
|
|
42
|
+
|
|
43
|
+
const result = await grantProvisionerRoles("p", "sa@p", {
|
|
44
|
+
run,
|
|
45
|
+
sleep: async (ms) => {
|
|
46
|
+
slept.push(ms);
|
|
47
|
+
},
|
|
48
|
+
retryDelaysMs: [10, 20, 40],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual({ granted: PROVISIONER_ROLE_LIST.length, failed: [] });
|
|
52
|
+
expect(calls.filter((r) => r === first)).toHaveLength(3);
|
|
53
|
+
expect(slept).toEqual([10, 20]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("retries concurrent policy changes", async () => {
|
|
57
|
+
const role = PROVISIONER_ROLE_LIST[1] as string;
|
|
58
|
+
const { run } = fakeGcloud({
|
|
59
|
+
[role]: [
|
|
60
|
+
fail(
|
|
61
|
+
"ERROR: ABORTED: There were concurrent policy changes. Please retry the whole read-modify-write with exponential backoff.",
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep });
|
|
67
|
+
|
|
68
|
+
expect(result.failed).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not retry a permission error and reports gcloud's message", async () => {
|
|
72
|
+
const role = PROVISIONER_ROLE_LIST[0] as string;
|
|
73
|
+
const { run, calls } = fakeGcloud({
|
|
74
|
+
[role]: [fail(PERMISSION_DENIED), fail(PERMISSION_DENIED)],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await grantProvisionerRoles("p", "sa@p", { run, sleep: noSleep });
|
|
78
|
+
|
|
79
|
+
expect(calls.filter((r) => r === role)).toHaveLength(1);
|
|
80
|
+
expect(result.granted).toBe(PROVISIONER_ROLE_LIST.length - 1);
|
|
81
|
+
expect(result.failed).toEqual([{ role, error: PERMISSION_DENIED }]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("gives up after the last retry and reports the last error", async () => {
|
|
85
|
+
const role = PROVISIONER_ROLE_LIST[0] as string;
|
|
86
|
+
const { run, calls } = fakeGcloud({
|
|
87
|
+
[role]: Array.from({ length: 10 }, () => fail(SA_NOT_YET_VISIBLE)),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await grantProvisionerRoles("p", "sa@p", {
|
|
91
|
+
run,
|
|
92
|
+
sleep: noSleep,
|
|
93
|
+
retryDelaysMs: [1, 1],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(calls.filter((r) => r === role)).toHaveLength(3);
|
|
97
|
+
expect(result.failed).toEqual([{ role, error: SA_NOT_YET_VISIBLE }]);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/gcloud.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin gcloud shell-out wrapper. The wizard composes high-level
|
|
5
|
+
* "create the project + link billing + enable APIs + create SA +
|
|
6
|
+
* grant roles" flows on top of these primitives. No key is ever minted
|
|
7
|
+
* for the provisioner SA: the operator impersonates it
|
|
8
|
+
* (provisioner-identity.ts, #696).
|
|
9
|
+
*
|
|
10
|
+
* Why shell out instead of using the GCP SDK directly:
|
|
11
|
+
* - the user's gcloud auth state IS the auth (no separate
|
|
12
|
+
* credentials handling — `gcloud` already knows the user)
|
|
13
|
+
* - project create + billing link must happen as the user, NOT as
|
|
14
|
+
* a service account (the SA doesn't exist yet at bootstrap time)
|
|
15
|
+
* - error messages from gcloud are operator-readable; the SDK's
|
|
16
|
+
* gRPC errors aren't
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
export interface GcloudResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
exitCode: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Environment for every non-interactive gcloud child process.
|
|
30
|
+
*
|
|
31
|
+
* `CLOUDSDK_CORE_DISABLE_PROMPTS=1` makes gcloud take the non-interactive
|
|
32
|
+
* path instead of asking a question nobody can see: our runners capture
|
|
33
|
+
* gcloud's output, so a "(Y/n)" prompt — e.g. "install the beta component?"
|
|
34
|
+
* — hung the CLI forever (`admin-domain enable`). With prompts disabled,
|
|
35
|
+
* gcloud fails fast with its error (or proceeds where the command allows it),
|
|
36
|
+
* and the caller reports the error. Commands that SHOULD talk to the
|
|
37
|
+
* operator (`gcloud auth login`) are never run through these runners; the
|
|
38
|
+
* wizard tells the operator to run them in their own terminal.
|
|
39
|
+
*/
|
|
40
|
+
export function gcloudEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
41
|
+
return { ...base, CLOUDSDK_CORE_DISABLE_PROMPTS: "1" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run a gcloud command. Buffered stdout + stderr (max 4 MB each).
|
|
46
|
+
* Throws on spawn failure but NOT on non-zero exit — caller checks `ok`.
|
|
47
|
+
* stdin is always ended (after `opts.stdin`, if given), so nothing can wait on it.
|
|
48
|
+
* `opts.env` replaces this process's environment as the base (prompts stay off).
|
|
49
|
+
*/
|
|
50
|
+
export async function gcloud(
|
|
51
|
+
args: string[],
|
|
52
|
+
opts: { stdin?: string; env?: NodeJS.ProcessEnv } = {},
|
|
53
|
+
): Promise<GcloudResult> {
|
|
54
|
+
return new Promise((resolveResult, reject) => {
|
|
55
|
+
const child = spawn("gcloud", args, {
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
env: gcloudEnv(opts.env ?? process.env),
|
|
58
|
+
});
|
|
59
|
+
const stdout: Buffer[] = [];
|
|
60
|
+
const stderr: Buffer[] = [];
|
|
61
|
+
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
|
|
62
|
+
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
|
|
63
|
+
child.on("error", (e) => reject(e));
|
|
64
|
+
child.on("close", (code) =>
|
|
65
|
+
resolveResult({
|
|
66
|
+
ok: code === 0,
|
|
67
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
68
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
69
|
+
exitCode: code ?? 1,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
child.stdin.end(opts.stdin);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Active gcloud account. Returns `null` when no account is logged in
|
|
78
|
+
* — caller prompts `gcloud auth login`.
|
|
79
|
+
*/
|
|
80
|
+
export async function activeAccount(): Promise<string | null> {
|
|
81
|
+
const r = await gcloud(["auth", "list", "--format=value(account)", "--filter=status:ACTIVE"]);
|
|
82
|
+
if (!r.ok) return null;
|
|
83
|
+
const value = r.stdout.trim();
|
|
84
|
+
return value.length > 0 ? value : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BillingAccount {
|
|
88
|
+
id: string;
|
|
89
|
+
displayName: string;
|
|
90
|
+
open: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function listBillingAccounts(): Promise<BillingAccount[]> {
|
|
94
|
+
const r = await gcloud(["billing", "accounts", "list", "--format=json"]);
|
|
95
|
+
if (!r.ok) return [];
|
|
96
|
+
try {
|
|
97
|
+
const rows = JSON.parse(r.stdout) as Array<{
|
|
98
|
+
name: string;
|
|
99
|
+
displayName: string;
|
|
100
|
+
open: boolean;
|
|
101
|
+
}>;
|
|
102
|
+
return rows.map((row) => ({
|
|
103
|
+
id: row.name.replace(/^billingAccounts\//, ""),
|
|
104
|
+
displayName: row.displayName,
|
|
105
|
+
open: row.open,
|
|
106
|
+
}));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function projectExists(projectId: string): Promise<boolean> {
|
|
113
|
+
const r = await gcloud(["projects", "describe", projectId, "--format=value(projectId)"]);
|
|
114
|
+
return r.ok && r.stdout.trim() === projectId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function createProject(projectId: string, displayName: string): Promise<GcloudResult> {
|
|
118
|
+
return gcloud(["projects", "create", projectId, "--name", displayName]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function linkBilling(
|
|
122
|
+
projectId: string,
|
|
123
|
+
billingAccountId: string,
|
|
124
|
+
): Promise<GcloudResult> {
|
|
125
|
+
return gcloud(["billing", "projects", "link", projectId, "--billing-account", billingAccountId]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const REQUIRED_APIS: readonly string[] = [
|
|
129
|
+
"compute.googleapis.com",
|
|
130
|
+
"sqladmin.googleapis.com",
|
|
131
|
+
"run.googleapis.com",
|
|
132
|
+
"secretmanager.googleapis.com",
|
|
133
|
+
"servicenetworking.googleapis.com",
|
|
134
|
+
"dns.googleapis.com",
|
|
135
|
+
"storage.googleapis.com",
|
|
136
|
+
"cloudresourcemanager.googleapis.com",
|
|
137
|
+
"iam.googleapis.com",
|
|
138
|
+
"cloudbuild.googleapis.com",
|
|
139
|
+
"artifactregistry.googleapis.com",
|
|
140
|
+
"bigquery.googleapis.com",
|
|
141
|
+
"iap.googleapis.com",
|
|
142
|
+
// Used by gcp.projects.ServiceIdentity to provision the IAP-managed
|
|
143
|
+
// service account that forwards authenticated requests to Cloud Run.
|
|
144
|
+
"serviceusage.googleapis.com",
|
|
145
|
+
// Hourly run of the operator-access sync job (operator-access.ts).
|
|
146
|
+
"cloudscheduler.googleapis.com",
|
|
147
|
+
// signJwt as caelo-mcp: how `staging-domain enable` passes the admin's
|
|
148
|
+
// IAP (staging-admin.ts, #678) — the same path the MCP shim takes.
|
|
149
|
+
"iamcredentials.googleapis.com",
|
|
150
|
+
// #696 — the KMS key Pulumi encrypts the stack's secrets with
|
|
151
|
+
// (ops-backend.ts; also enabled there for installs that predate this).
|
|
152
|
+
"cloudkms.googleapis.com",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// v0.3.1 — gcp-firebase provider needs the Firebase Management +
|
|
156
|
+
// Firebase Hosting APIs on top of the gcp baseline. Without these
|
|
157
|
+
// the Pulumi `gcp.firebase.HostingSite` resource fails to create.
|
|
158
|
+
const GCP_FIREBASE_EXTRA_APIS: readonly string[] = [
|
|
159
|
+
"firebase.googleapis.com",
|
|
160
|
+
"firebasehosting.googleapis.com",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
export async function enableApis(
|
|
164
|
+
projectId: string,
|
|
165
|
+
provider: "gcp" | "gcp-firebase" = "gcp",
|
|
166
|
+
): Promise<GcloudResult> {
|
|
167
|
+
const apis =
|
|
168
|
+
provider === "gcp-firebase" ? [...REQUIRED_APIS, ...GCP_FIREBASE_EXTRA_APIS] : REQUIRED_APIS;
|
|
169
|
+
return gcloud(["services", "enable", ...apis, "--project", projectId]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function serviceAccountExists(projectId: string, saEmail: string): Promise<boolean> {
|
|
173
|
+
const r = await gcloud([
|
|
174
|
+
"iam",
|
|
175
|
+
"service-accounts",
|
|
176
|
+
"describe",
|
|
177
|
+
saEmail,
|
|
178
|
+
"--project",
|
|
179
|
+
projectId,
|
|
180
|
+
"--format=value(email)",
|
|
181
|
+
]);
|
|
182
|
+
return r.ok && r.stdout.trim() === saEmail;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function createServiceAccount(
|
|
186
|
+
projectId: string,
|
|
187
|
+
accountId: string,
|
|
188
|
+
displayName: string,
|
|
189
|
+
): Promise<GcloudResult> {
|
|
190
|
+
return gcloud([
|
|
191
|
+
"iam",
|
|
192
|
+
"service-accounts",
|
|
193
|
+
"create",
|
|
194
|
+
accountId,
|
|
195
|
+
"--display-name",
|
|
196
|
+
displayName,
|
|
197
|
+
"--project",
|
|
198
|
+
projectId,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const PROVISIONER_ROLES: readonly string[] = [
|
|
203
|
+
"roles/run.admin",
|
|
204
|
+
"roles/cloudsql.admin",
|
|
205
|
+
"roles/storage.admin",
|
|
206
|
+
"roles/secretmanager.admin",
|
|
207
|
+
"roles/iam.serviceAccountUser",
|
|
208
|
+
"roles/compute.networkAdmin",
|
|
209
|
+
"roles/dns.admin",
|
|
210
|
+
"roles/servicenetworking.networksAdmin",
|
|
211
|
+
"roles/iam.serviceAccountTokenCreator",
|
|
212
|
+
"roles/cloudbuild.builds.editor",
|
|
213
|
+
"roles/artifactregistry.admin",
|
|
214
|
+
"roles/compute.securityAdmin",
|
|
215
|
+
"roles/iam.serviceAccountAdmin",
|
|
216
|
+
"roles/bigquery.admin",
|
|
217
|
+
"roles/iap.admin",
|
|
218
|
+
// compute.admin includes RegionNetworkEndpointGroups + URL maps +
|
|
219
|
+
// BackendService variants the LB needs. compute.networkAdmin alone
|
|
220
|
+
// doesn't cover NEG create.
|
|
221
|
+
"roles/compute.admin",
|
|
222
|
+
// logging.configWriter creates ProjectSink (BigQuery edge logs).
|
|
223
|
+
"roles/logging.configWriter",
|
|
224
|
+
// serviceusage.serviceUsageAdmin lets us trigger the IAP managed
|
|
225
|
+
// service identity (gcp.projects.ServiceIdentity).
|
|
226
|
+
"roles/serviceusage.serviceUsageAdmin",
|
|
227
|
+
// v0.3.6 — Firebase Hosting + Firebase project administration
|
|
228
|
+
// for the gcp-firebase provider variant. The stack's
|
|
229
|
+
// `gcp.firebase.Project` (which adds Firebase services to a GCP
|
|
230
|
+
// project) + `gcp.firebase.HostingSite` need these roles.
|
|
231
|
+
// Granting on gcp installs is a harmless no-op since those
|
|
232
|
+
// resources aren't created on that stack.
|
|
233
|
+
"roles/firebase.admin",
|
|
234
|
+
"roles/firebasehosting.admin",
|
|
235
|
+
// v0.3.7 — project-level IAM binding admin. Required so the
|
|
236
|
+
// provisioner SA can grant other SAs roles AT THE PROJECT LEVEL
|
|
237
|
+
// via `gcp.projects.IAMMember`. The gcp-firebase stack uses
|
|
238
|
+
// this for `roles/firebasehosting.admin` on the run SA so it
|
|
239
|
+
// can deploy to Firebase Hosting. Without this role the
|
|
240
|
+
// provisioner gets a 403 "Policy update access denied" when
|
|
241
|
+
// creating project-level IAM bindings. Secret/bucket/SA-level
|
|
242
|
+
// bindings already work via the per-resource admin roles above.
|
|
243
|
+
"roles/resourcemanager.projectIamAdmin",
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* IAM errors that clear up on their own within seconds:
|
|
248
|
+
* - a service account created moments ago is not yet visible to the
|
|
249
|
+
* IAM policy backend ("Service account ... does not exist") — this
|
|
250
|
+
* hits the FIRST binding right after `createServiceAccount`
|
|
251
|
+
* - Google's service agents write the project policy concurrently
|
|
252
|
+
* right after `enableApis` ("There were concurrent policy changes")
|
|
253
|
+
* Anything else (PERMISSION_DENIED, bad role name) fails immediately.
|
|
254
|
+
*/
|
|
255
|
+
const TRANSIENT_IAM_ERROR =
|
|
256
|
+
/does not exist|concurrent policy changes|ABORTED|UNAVAILABLE|deadline exceeded/i;
|
|
257
|
+
|
|
258
|
+
const GRANT_RETRY_DELAYS_MS: readonly number[] = [2_000, 4_000, 8_000, 16_000];
|
|
259
|
+
|
|
260
|
+
interface RoleGrantFailure {
|
|
261
|
+
role: string;
|
|
262
|
+
/** Last gcloud stderr for this role, so the operator sees WHY. */
|
|
263
|
+
error: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Bind every role the GCP stack provisioner SA needs. Idempotent —
|
|
268
|
+
* gcloud silently no-ops a binding that already exists. Transient IAM
|
|
269
|
+
* errors are retried with backoff; `opts` exists for tests.
|
|
270
|
+
*/
|
|
271
|
+
export async function grantProvisionerRoles(
|
|
272
|
+
projectId: string,
|
|
273
|
+
saEmail: string,
|
|
274
|
+
opts: {
|
|
275
|
+
run?: typeof gcloud;
|
|
276
|
+
sleep?: (ms: number) => Promise<void>;
|
|
277
|
+
retryDelaysMs?: readonly number[];
|
|
278
|
+
/** Roles to bind instead of the provisioner's base set (the install job's extras). */
|
|
279
|
+
roles?: readonly string[];
|
|
280
|
+
} = {},
|
|
281
|
+
): Promise<{ granted: number; failed: RoleGrantFailure[] }> {
|
|
282
|
+
const run = opts.run ?? gcloud;
|
|
283
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
284
|
+
const retryDelaysMs = opts.retryDelaysMs ?? GRANT_RETRY_DELAYS_MS;
|
|
285
|
+
let granted = 0;
|
|
286
|
+
const failed: RoleGrantFailure[] = [];
|
|
287
|
+
for (const role of opts.roles ?? PROVISIONER_ROLES) {
|
|
288
|
+
const args = [
|
|
289
|
+
"projects",
|
|
290
|
+
"add-iam-policy-binding",
|
|
291
|
+
projectId,
|
|
292
|
+
"--member",
|
|
293
|
+
`serviceAccount:${saEmail}`,
|
|
294
|
+
"--role",
|
|
295
|
+
role,
|
|
296
|
+
"--condition=None",
|
|
297
|
+
"--quiet",
|
|
298
|
+
];
|
|
299
|
+
let r = await run(args);
|
|
300
|
+
for (const delay of retryDelaysMs) {
|
|
301
|
+
if (r.ok || !TRANSIENT_IAM_ERROR.test(r.stderr)) break;
|
|
302
|
+
await sleep(delay);
|
|
303
|
+
r = await run(args);
|
|
304
|
+
}
|
|
305
|
+
if (r.ok) granted++;
|
|
306
|
+
else failed.push({ role, error: r.stderr.trim() });
|
|
307
|
+
}
|
|
308
|
+
return { granted, failed };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const REQUIRED_API_LIST = REQUIRED_APIS;
|
|
312
|
+
export const GCP_FIREBASE_EXTRA_API_LIST = GCP_FIREBASE_EXTRA_APIS;
|
|
313
|
+
export const PROVISIONER_ROLE_LIST = PROVISIONER_ROLES;
|
|
314
|
+
|
|
315
|
+
// v0.3.12 — removed createFirebaseHostingIdentity. The Service
|
|
316
|
+
// Usage GenerateServiceIdentity API doesn't support
|
|
317
|
+
// `firebasehosting.googleapis.com`, and `gcloud beta services
|
|
318
|
+
// identity create` would have returned the same error after a
|
|
319
|
+
// 2-min beta-component download. The SA gets auto-created lazily
|
|
320
|
+
// by Firebase Hosting on first deploy. We grant run.invoker at
|
|
321
|
+
// the project level instead of service level so the binding
|
|
322
|
+
// doesn't require the SA to exist at bind time.
|
package/src/gcp-names.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic names of the GCP resources the `gcp` and `gcp-firebase`
|
|
5
|
+
* Pulumi stacks create. The stacks build these names with the helpers below
|
|
6
|
+
* and `cms-provision upgrade` addresses the same resources through gcloud, so
|
|
7
|
+
* the two can never disagree about which bucket, secret or service account
|
|
8
|
+
* they mean.
|
|
9
|
+
*
|
|
10
|
+
* Only names that are fixed strings belong here. Pulumi auto-names (the
|
|
11
|
+
* random suffix on Cloud Run services, backend buckets, ...) are discovered at
|
|
12
|
+
* upgrade time instead.
|
|
13
|
+
*
|
|
14
|
+
* Pure module — the stacks import it from `dist/`, so it must not pull in
|
|
15
|
+
* the CLI's dependencies.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The Pulumi stack name the wizard provisions and `upgrade` operates on. */
|
|
19
|
+
export const GCP_STACK_ENV = "production";
|
|
20
|
+
|
|
21
|
+
/** Account id of the service account external MCP clients sign as (issue #37). */
|
|
22
|
+
export const MCP_IAP_SERVICE_ACCOUNT_ID = "caelo-mcp";
|
|
23
|
+
|
|
24
|
+
/** Email of the MCP service account in `projectId`. */
|
|
25
|
+
export function mcpIapServiceAccountEmail(projectId: string): string {
|
|
26
|
+
return `${MCP_IAP_SERVICE_ACCOUNT_ID}@${projectId}.iam.gserviceaccount.com`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `caelo-<env>`: prefix of every stack resource name. */
|
|
30
|
+
export function gcpNamePrefix(env: string): string {
|
|
31
|
+
return `caelo-${env}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Account id of the runtime service account admin + gateway run as. */
|
|
35
|
+
export function runServiceAccountId(env: string): string {
|
|
36
|
+
return `${gcpNamePrefix(env)}-run-sa`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Email of the runtime service account admin + gateway run as. */
|
|
40
|
+
export function runServiceAccountEmail(projectId: string, env: string): string {
|
|
41
|
+
return `${runServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Account id of the service account the gateway runs as. Separate from the
|
|
46
|
+
* admin's so the public-facing gateway can read only the secrets it needs
|
|
47
|
+
* (stack-contract.ts `SERVICE_SECRET_ENV`), never the KEK or the admin-only
|
|
48
|
+
* secrets.
|
|
49
|
+
*/
|
|
50
|
+
export function gatewayServiceAccountId(env: string): string {
|
|
51
|
+
return `${gcpNamePrefix(env)}-gateway-sa`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Email of the service account the gateway runs as. */
|
|
55
|
+
export function gatewayServiceAccountEmail(projectId: string, env: string): string {
|
|
56
|
+
return `${gatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Account id of the `gcp` stack's static-publisher service account. Account
|
|
61
|
+
* ids max out at 30 chars, so the env is shortened.
|
|
62
|
+
*/
|
|
63
|
+
export function staticPublisherServiceAccountId(env: string): string {
|
|
64
|
+
const short = env === "production" ? "prod" : env === "staging" ? "stg" : "dev";
|
|
65
|
+
return `caelo-${short}-publisher`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Globally unique GCS bucket name the stacks give each bucket. */
|
|
69
|
+
export function gcpBucketName(
|
|
70
|
+
projectId: string,
|
|
71
|
+
env: string,
|
|
72
|
+
bucket: "media" | "static" | "staging",
|
|
73
|
+
): string {
|
|
74
|
+
return `${projectId}-${gcpNamePrefix(env)}-${bucket}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Secret Manager secret id for one of the stack's runtime secrets. */
|
|
78
|
+
export function gcpSecretId(env: string, name: string): string {
|
|
79
|
+
return `${gcpNamePrefix(env)}-${name}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Cloud Run job that keeps Google IAP operator access in sync (operator-access.ts). */
|
|
83
|
+
export function operatorAccessJobName(env: string): string {
|
|
84
|
+
return `${gcpNamePrefix(env)}-operator-access-sync`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Full resource name of the operator-access sync job, as the admin's env carries it. */
|
|
88
|
+
export function operatorAccessJobResource(projectId: string, region: string, env: string): string {
|
|
89
|
+
return `projects/${projectId}/locations/${region}/jobs/${operatorAccessJobName(env)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Account id of the sync job's own service account — the only principal that
|
|
94
|
+
* may change the admin's IAP binding and the caelo-mcp token-creator binding.
|
|
95
|
+
*/
|
|
96
|
+
export function operatorAccessServiceAccountId(env: string): string {
|
|
97
|
+
return `${gcpNamePrefix(env)}-opaccess`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Email of the sync job's service account. */
|
|
101
|
+
export function operatorAccessServiceAccountEmail(projectId: string, env: string): string {
|
|
102
|
+
return `${operatorAccessServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Cloud Scheduler job that runs the sync job hourly (self-healing). */
|
|
106
|
+
export function operatorAccessScheduleName(env: string): string {
|
|
107
|
+
return `${gcpNamePrefix(env)}-operator-access-hourly`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Cloud Run job that installs Caelo releases from the admin (epic #696):
|
|
112
|
+
* the admin may only start it, without overrides (upgrader-setup.ts).
|
|
113
|
+
*/
|
|
114
|
+
export function upgraderJobName(env: string): string {
|
|
115
|
+
return `${gcpNamePrefix(env)}-upgrader`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Full resource name of the upgrader job, as the admin's env carries it. */
|
|
119
|
+
export function upgraderJobResource(projectId: string, region: string, env: string): string {
|
|
120
|
+
return `projects/${projectId}/locations/${region}/jobs/${upgraderJobName(env)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Account id of the upgrader job's own service account: may roll the two
|
|
125
|
+
* Cloud Run services, run the migration job and back up Cloud SQL, never
|
|
126
|
+
* change IAM, service accounts or secrets (upgrader-setup.ts).
|
|
127
|
+
*/
|
|
128
|
+
export function upgraderServiceAccountId(env: string): string {
|
|
129
|
+
return `${gcpNamePrefix(env)}-upgrader`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Email of the upgrader job's service account. */
|
|
133
|
+
export function upgraderServiceAccountEmail(projectId: string, env: string): string {
|
|
134
|
+
return `${upgraderServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
135
|
+
}
|