@caelo-cms/provisioning 0.10.46 → 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/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- 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 +122 -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 +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- 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/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.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 +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- 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 +291 -83
- 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,332 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The operator-access sync job's setup (operator-access.ts) against a fake
|
|
5
|
+
* gcloud. The load-bearing assertions are the trust boundary: only the job's
|
|
6
|
+
* own service account gets IAM-policy rights, and only on the admin's IAP
|
|
7
|
+
* resource and the caelo-mcp account; the admin's run SA ends up able to
|
|
8
|
+
* start the job and read its runs — nothing else — and loses what an earlier
|
|
9
|
+
* revision gave it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { withIamAuthFlag } from "./cloudsql-iam-user.js";
|
|
14
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
15
|
+
import {
|
|
16
|
+
ensureOperatorAccessSync,
|
|
17
|
+
installIapAllowlist,
|
|
18
|
+
OPERATOR_ACCESS_ROLE,
|
|
19
|
+
type OperatorAccessSyncTarget,
|
|
20
|
+
resolveOperatorAccessTarget,
|
|
21
|
+
} from "./operator-access.js";
|
|
22
|
+
|
|
23
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
24
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
25
|
+
|
|
26
|
+
function fakeGcloud(answers: Record<string, GcloudResult[]>) {
|
|
27
|
+
const calls: string[][] = [];
|
|
28
|
+
const run = async (args: string[]) => {
|
|
29
|
+
calls.push(args);
|
|
30
|
+
const key = Object.keys(answers)
|
|
31
|
+
.sort((a, b) => b.length - a.length)
|
|
32
|
+
.find((k) => args.join(" ").startsWith(k));
|
|
33
|
+
return (key && answers[key]?.shift()) || ok();
|
|
34
|
+
};
|
|
35
|
+
return { run, calls, lines: () => calls.map((c) => c.join(" ")) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const JOB_SA = "caelo-production-opaccess@p.iam.gserviceaccount.com";
|
|
39
|
+
const RUN_SA = "caelo-production-run-sa@p.iam.gserviceaccount.com";
|
|
40
|
+
const CUSTOM = "projects/p/roles/caeloOperatorAccess";
|
|
41
|
+
|
|
42
|
+
const target: OperatorAccessSyncTarget = {
|
|
43
|
+
projectId: "p",
|
|
44
|
+
region: "europe-west1",
|
|
45
|
+
env: "production",
|
|
46
|
+
ownerEmail: "Owner@x.com",
|
|
47
|
+
resource: { kind: "cloud-run", service: "caelo-production-admin-abc", region: "europe-west1" },
|
|
48
|
+
imageRef: "europe-west1-docker.pkg.dev/r/admin@sha256:new",
|
|
49
|
+
network: "net",
|
|
50
|
+
subnet: "sub",
|
|
51
|
+
databaseHost: "10.0.0.3",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const roleJson = JSON.stringify({ includedPermissions: [...OPERATOR_ACCESS_ROLE.permissions] });
|
|
55
|
+
const legacyIapPolicy = JSON.stringify({
|
|
56
|
+
bindings: [{ role: CUSTOM, members: [`serviceAccount:${RUN_SA}`] }],
|
|
57
|
+
});
|
|
58
|
+
const flagsJson = JSON.stringify({
|
|
59
|
+
settings: { databaseFlags: [{ name: "max_connections", value: "50" }] },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const freshInstall = () =>
|
|
63
|
+
fakeGcloud({
|
|
64
|
+
"iam roles describe": [ok(roleJson)],
|
|
65
|
+
"iap web get-iam-policy": [ok(legacyIapPolicy)],
|
|
66
|
+
"iam service-accounts get-iam-policy": [ok("{}")],
|
|
67
|
+
"projects get-iam-policy": [ok("{}")],
|
|
68
|
+
"sql instances list": [ok("caelo-production-pg-1a2b\n")],
|
|
69
|
+
"sql instances describe": [ok(flagsJson)],
|
|
70
|
+
"sql users list": [ok("admin_role\npublic_role\n")],
|
|
71
|
+
"scheduler jobs describe": [fail("NOT_FOUND")],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Every IAM-binding add as {resource words, member, role}. */
|
|
75
|
+
function grants(calls: string[][]) {
|
|
76
|
+
return calls
|
|
77
|
+
.filter((c) => c.includes("add-iam-policy-binding"))
|
|
78
|
+
.map((c) => ({
|
|
79
|
+
on: c.slice(0, c.indexOf("add-iam-policy-binding") + 2).join(" "),
|
|
80
|
+
member: c.find((a) => a.startsWith("--member="))?.slice(9),
|
|
81
|
+
role: c.find((a) => a.startsWith("--role="))?.slice(7),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("ensureOperatorAccessSync", () => {
|
|
86
|
+
it("gives IAM-policy rights to the job's own SA only, on exactly the IAP resource and caelo-mcp", async () => {
|
|
87
|
+
const { run, calls } = freshInstall();
|
|
88
|
+
const r = await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
89
|
+
expect(r.ok).toBe(true);
|
|
90
|
+
const custom = grants(calls).filter((g) => g.role === CUSTOM);
|
|
91
|
+
expect(custom).toEqual([
|
|
92
|
+
{
|
|
93
|
+
on: "iap web add-iam-policy-binding --resource-type=cloud-run",
|
|
94
|
+
member: `serviceAccount:${JOB_SA}`,
|
|
95
|
+
role: CUSTOM,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
on: "iam service-accounts add-iam-policy-binding caelo-mcp@p.iam.gserviceaccount.com",
|
|
99
|
+
member: `serviceAccount:${JOB_SA}`,
|
|
100
|
+
role: CUSTOM,
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
// Nothing that can write IAM policy goes to the project or to anyone else.
|
|
104
|
+
expect(
|
|
105
|
+
grants(calls)
|
|
106
|
+
.filter((g) => g.on.startsWith("projects"))
|
|
107
|
+
.map((g) => g.role),
|
|
108
|
+
).toEqual(["roles/cloudsql.instanceUser", "roles/logging.logWriter"]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("leaves the admin's run SA able to start the job and read its runs — nothing else", async () => {
|
|
112
|
+
const { run, calls } = freshInstall();
|
|
113
|
+
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
114
|
+
expect(grants(calls).filter((g) => g.member === `serviceAccount:${RUN_SA}`)).toEqual([
|
|
115
|
+
{
|
|
116
|
+
on: "run jobs add-iam-policy-binding caelo-production-operator-access-sync",
|
|
117
|
+
member: `serviceAccount:${RUN_SA}`,
|
|
118
|
+
role: "roles/run.jobsExecutor",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
on: "run jobs add-iam-policy-binding caelo-production-operator-access-sync",
|
|
122
|
+
member: `serviceAccount:${RUN_SA}`,
|
|
123
|
+
role: "roles/run.viewer",
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
// jobsExecutor carries run.jobs.run but not run.jobs.runWithOverrides.
|
|
127
|
+
expect(calls.flat().some((a) => /WithOverrides|run\.developer|run\.admin/.test(a))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("removes the operator-access grant an earlier revision gave the admin's run SA", async () => {
|
|
131
|
+
const { run, lines } = freshInstall();
|
|
132
|
+
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
133
|
+
const removals = lines().filter((l) => l.includes("remove-iam-policy-binding"));
|
|
134
|
+
expect(removals).toHaveLength(1);
|
|
135
|
+
expect(removals[0]).toContain("iap web remove-iam-policy-binding");
|
|
136
|
+
expect(removals[0]).toContain(`--member=serviceAccount:${RUN_SA}`);
|
|
137
|
+
expect(removals[0]).toContain(`--role=${CUSTOM}`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("turns on IAM database auth without dropping existing flags, and makes the job a read-only DB user", async () => {
|
|
141
|
+
const { run, lines } = freshInstall();
|
|
142
|
+
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
143
|
+
expect(lines()).toContain(
|
|
144
|
+
"sql instances patch caelo-production-pg-1a2b --project=p --database-flags=max_connections=50,cloudsql.iam_authentication=on --quiet",
|
|
145
|
+
);
|
|
146
|
+
expect(lines()).toContain(
|
|
147
|
+
"sql users create caelo-production-opaccess@p.iam --instance=caelo-production-pg-1a2b --type=cloud_iam_service_account --project=p --quiet",
|
|
148
|
+
);
|
|
149
|
+
expect(lines()).toContain(
|
|
150
|
+
"sql users assign-roles caelo-production-opaccess@p.iam --instance=caelo-production-pg-1a2b --type=cloud_iam_service_account --database-roles=operator_access_reader --project=p --quiet",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("deploys the job from the admin image as its own SA, with its whole input as env", async () => {
|
|
155
|
+
const { run, calls } = freshInstall();
|
|
156
|
+
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
157
|
+
const deploy = calls.find((c) => c.slice(0, 3).join(" ") === "run jobs deploy");
|
|
158
|
+
expect(deploy).toContain(`--service-account=${JOB_SA}`);
|
|
159
|
+
expect(deploy).toContain(`--image=${target.imageRef}`);
|
|
160
|
+
expect(deploy).toContain(
|
|
161
|
+
"--args=--bun,/app/packages/admin-core/src/security/operator-access/sync-job.ts",
|
|
162
|
+
);
|
|
163
|
+
expect(deploy).toContain(
|
|
164
|
+
"--set-env-vars=^|^CAELO_OPERATOR_ACCESS_IAP_WEB=cloud_run-europe-west1/services/caelo-production-admin-abc|CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@p.iam.gserviceaccount.com|CAELO_OPERATOR_ACCESS_STATIC_MEMBERS=user:owner@x.com|CAELO_OPERATOR_ACCESS_DB_HOST=10.0.0.3",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("schedules an hourly run as the job's own SA", async () => {
|
|
169
|
+
const { run, lines } = freshInstall();
|
|
170
|
+
await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
171
|
+
const create = lines().find((l) => l.startsWith("scheduler jobs create http"));
|
|
172
|
+
expect(create).toContain("--schedule=17 * * * *");
|
|
173
|
+
expect(create).toContain(
|
|
174
|
+
"--uri=https://run.googleapis.com/v2/projects/p/locations/europe-west1/jobs/caelo-production-operator-access-sync:run",
|
|
175
|
+
);
|
|
176
|
+
expect(create).toContain(`--oauth-service-account-email=${JOB_SA}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("is idempotent on a converged install: no flag patch, no user create, scheduler updated", async () => {
|
|
180
|
+
const { run, lines } = fakeGcloud({
|
|
181
|
+
"iam roles describe": [ok(roleJson)],
|
|
182
|
+
"sql instances list": [ok("caelo-production-pg-1a2b\n")],
|
|
183
|
+
"sql instances describe": [
|
|
184
|
+
ok(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
settings: {
|
|
187
|
+
databaseFlags: [
|
|
188
|
+
{ name: "max_connections", value: "50" },
|
|
189
|
+
{ name: "cloudsql.iam_authentication", value: "on" },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
"sql users list": [ok("caelo-production-opaccess@p.iam\n")],
|
|
196
|
+
"scheduler jobs describe": [ok("projects/p/locations/europe-west1/jobs/x")],
|
|
197
|
+
});
|
|
198
|
+
const r = await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
199
|
+
expect(r.ok).toBe(true);
|
|
200
|
+
expect(lines().some((l) => l.startsWith("sql instances patch"))).toBe(false);
|
|
201
|
+
expect(lines().some((l) => l.startsWith("sql users create"))).toBe(false);
|
|
202
|
+
expect(lines().some((l) => l.includes("remove-iam-policy-binding"))).toBe(false);
|
|
203
|
+
expect(lines().some((l) => l.startsWith("scheduler jobs update http"))).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("fails loudly naming the step, and reports what was already done", async () => {
|
|
207
|
+
const { run } = fakeGcloud({
|
|
208
|
+
"iam roles describe": [ok(roleJson)],
|
|
209
|
+
"sql instances list": [ok("caelo-production-pg-1a2b\n")],
|
|
210
|
+
"sql instances describe": [ok(flagsJson)],
|
|
211
|
+
"run jobs deploy": [fail("PERMISSION_DENIED: run.jobs.create")],
|
|
212
|
+
});
|
|
213
|
+
const r = await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
214
|
+
expect(r.ok).toBe(false);
|
|
215
|
+
if (!r.ok) {
|
|
216
|
+
expect(r.error).toBe(
|
|
217
|
+
"deploy job caelo-production-operator-access-sync: PERMISSION_DENIED: run.jobs.create",
|
|
218
|
+
);
|
|
219
|
+
expect(r.done).toContain("custom role caeloOperatorAccess");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("refuses to guess the Cloud SQL instance", async () => {
|
|
224
|
+
const { run } = fakeGcloud({
|
|
225
|
+
"iam roles describe": [ok(roleJson)],
|
|
226
|
+
"sql instances list": [ok("caelo-production-pg-a\ncaelo-production-pg-b\n")],
|
|
227
|
+
});
|
|
228
|
+
const r = await ensureOperatorAccessSync(target, { run, sleep: async () => {} });
|
|
229
|
+
expect(r.ok === false && r.error).toContain("exactly one caelo-production-pg*");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("withIamAuthFlag", () => {
|
|
234
|
+
it("keeps every existing flag and adds the IAM auth flag once", () => {
|
|
235
|
+
expect(withIamAuthFlag([{ name: "max_connections", value: "50" }])).toEqual([
|
|
236
|
+
{ name: "max_connections", value: "50" },
|
|
237
|
+
{ name: "cloudsql.iam_authentication", value: "on" },
|
|
238
|
+
]);
|
|
239
|
+
expect(withIamAuthFlag([{ name: "cloudsql.iam_authentication", value: "on" }])).toBeNull();
|
|
240
|
+
expect(withIamAuthFlag([{ name: "cloudsql.iam_authentication", value: "off" }])).toEqual([
|
|
241
|
+
{ name: "cloudsql.iam_authentication", value: "on" },
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("installIapAllowlist", () => {
|
|
247
|
+
it("is the owner, as the wizard passes it to the stack and the job keeps it", () => {
|
|
248
|
+
expect(installIapAllowlist(" Owner@X.com ")).toEqual(["user:owner@x.com"]);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("resolveOperatorAccessTarget", () => {
|
|
253
|
+
const adminJson = JSON.stringify({
|
|
254
|
+
spec: {
|
|
255
|
+
template: {
|
|
256
|
+
metadata: {
|
|
257
|
+
annotations: {
|
|
258
|
+
"run.googleapis.com/network-interfaces": JSON.stringify([
|
|
259
|
+
{
|
|
260
|
+
network: "projects/p/global/networks/net",
|
|
261
|
+
subnetwork: "projects/p/regions/r/subnetworks/sub",
|
|
262
|
+
},
|
|
263
|
+
]),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
spec: {
|
|
267
|
+
containers: [
|
|
268
|
+
{
|
|
269
|
+
image: "img@sha256:live",
|
|
270
|
+
env: [
|
|
271
|
+
{
|
|
272
|
+
name: "ADMIN_DATABASE_URL",
|
|
273
|
+
value: "postgresql://admin_role@10.0.0.3:5432/cms_admin",
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const base = { projectId: "p", region: "europe-west1", env: "production", ownerEmail: "o@x.com" };
|
|
283
|
+
|
|
284
|
+
it("gcp: targets the single IAP-enabled admin backend service", async () => {
|
|
285
|
+
const { run } = fakeGcloud({
|
|
286
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
287
|
+
"run services describe": [ok(adminJson)],
|
|
288
|
+
"compute backend-services list": [ok("caelo-production-admin-backend-1a2b\n")],
|
|
289
|
+
});
|
|
290
|
+
const r = await resolveOperatorAccessTarget(
|
|
291
|
+
{ ...base, provider: "gcp", imageRef: "img@sha256:new" },
|
|
292
|
+
{ run },
|
|
293
|
+
);
|
|
294
|
+
expect(r).toEqual({
|
|
295
|
+
ok: true,
|
|
296
|
+
target: {
|
|
297
|
+
...base,
|
|
298
|
+
resource: { kind: "backend-services", service: "caelo-production-admin-backend-1a2b" },
|
|
299
|
+
imageRef: "img@sha256:new",
|
|
300
|
+
network: "net",
|
|
301
|
+
subnet: "sub",
|
|
302
|
+
databaseHost: "10.0.0.3",
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("gcp-firebase: the admin service itself is the IAP resource; refuses an ambiguous service", async () => {
|
|
308
|
+
const one = fakeGcloud({
|
|
309
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
310
|
+
"run services describe": [ok(adminJson)],
|
|
311
|
+
});
|
|
312
|
+
const r = await resolveOperatorAccessTarget(
|
|
313
|
+
{ ...base, provider: "gcp-firebase" },
|
|
314
|
+
{ run: one.run },
|
|
315
|
+
);
|
|
316
|
+
expect(r.ok && r.target.resource).toEqual({
|
|
317
|
+
kind: "cloud-run",
|
|
318
|
+
service: "caelo-production-admin-abc",
|
|
319
|
+
region: "europe-west1",
|
|
320
|
+
});
|
|
321
|
+
expect(r.ok && r.target.imageRef).toBe("img@sha256:live");
|
|
322
|
+
|
|
323
|
+
const two = fakeGcloud({
|
|
324
|
+
"run services list": [ok("caelo-production-admin-a\ncaelo-production-admin-b\n")],
|
|
325
|
+
});
|
|
326
|
+
const r2 = await resolveOperatorAccessTarget(
|
|
327
|
+
{ ...base, provider: "gcp-firebase" },
|
|
328
|
+
{ run: two.run },
|
|
329
|
+
);
|
|
330
|
+
expect(r2.ok === false && r2.error).toContain("exactly one");
|
|
331
|
+
});
|
|
332
|
+
});
|