@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,320 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The install's ops bucket + Pulumi state key (#696, ops-backend.ts) against
|
|
5
|
+
* a stateful fake gcloud. Load-bearing: a fresh project ends with a
|
|
6
|
+
* versioned, uniform-access, public-access-prevented bucket and a KMS key
|
|
7
|
+
* that only the operator and caelo-provisioner may use; a second run writes
|
|
8
|
+
* nothing; a public bucket is refused; a dry run only reads; nothing is
|
|
9
|
+
* ever deleted.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureOpsBackend,
|
|
16
|
+
isOpsLifecycle,
|
|
17
|
+
NONCURRENT_VERSION_DAYS,
|
|
18
|
+
OPS_BUCKET_ROLE,
|
|
19
|
+
OPS_KEY_ROLE,
|
|
20
|
+
} from "./ops-backend.js";
|
|
21
|
+
import { opsKmsKeyName } from "./state-backend.js";
|
|
22
|
+
import { estimateGcpCost } from "./wizards/gcp-cost.js";
|
|
23
|
+
|
|
24
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
25
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
26
|
+
|
|
27
|
+
type Binding = { role: string; members: string[] };
|
|
28
|
+
|
|
29
|
+
/** A tiny in-memory GCP: one bucket, one key ring, one key, their policies. */
|
|
30
|
+
function fakeGcp(initial: { bucket?: boolean; ring?: boolean; key?: boolean } = {}) {
|
|
31
|
+
const state = {
|
|
32
|
+
apis: new Set<string>(
|
|
33
|
+
initial.bucket ? ["storage.googleapis.com", "cloudkms.googleapis.com"] : [],
|
|
34
|
+
),
|
|
35
|
+
bucket: initial.bucket
|
|
36
|
+
? {
|
|
37
|
+
versioning_enabled: true,
|
|
38
|
+
uniform_bucket_level_access: true,
|
|
39
|
+
public_access_prevention: "enforced",
|
|
40
|
+
lifecycle_config: {
|
|
41
|
+
rule: [
|
|
42
|
+
{
|
|
43
|
+
action: { type: "Delete" },
|
|
44
|
+
condition: { daysSinceNoncurrentTime: 30, isLive: false },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
: null,
|
|
50
|
+
bucketPolicy: [] as Binding[],
|
|
51
|
+
/** The bucket name exists, but in someone else's project. */
|
|
52
|
+
foreignBucket: false,
|
|
53
|
+
ring: initial.ring ?? false,
|
|
54
|
+
key: initial.key ?? false,
|
|
55
|
+
keyPolicy: [] as Binding[],
|
|
56
|
+
};
|
|
57
|
+
const calls: string[] = [];
|
|
58
|
+
const flag = (args: string[], name: string) =>
|
|
59
|
+
args.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
|
|
60
|
+
const bind = (policy: Binding[], role: string, member: string) => {
|
|
61
|
+
const b = policy.find((x) => x.role === role);
|
|
62
|
+
if (b) {
|
|
63
|
+
if (!b.members.includes(member)) b.members.push(member);
|
|
64
|
+
} else policy.push({ role, members: [member] });
|
|
65
|
+
};
|
|
66
|
+
const run = async (args: string[]): Promise<GcloudResult> => {
|
|
67
|
+
const line = args.join(" ");
|
|
68
|
+
calls.push(line);
|
|
69
|
+
if (line.startsWith("services list")) return ok([...state.apis].join("\n"));
|
|
70
|
+
if (line.startsWith("services enable")) {
|
|
71
|
+
for (const a of args.slice(2)) if (!a.startsWith("--")) state.apis.add(a);
|
|
72
|
+
return ok();
|
|
73
|
+
}
|
|
74
|
+
if (line.startsWith("storage buckets describe")) {
|
|
75
|
+
return state.bucket ? ok(JSON.stringify(state.bucket)) : fail("ERROR: NotFound: 404 bucket");
|
|
76
|
+
}
|
|
77
|
+
if (line.startsWith("storage buckets create")) {
|
|
78
|
+
state.bucket = {
|
|
79
|
+
versioning_enabled: false,
|
|
80
|
+
uniform_bucket_level_access: args.includes("--uniform-bucket-level-access"),
|
|
81
|
+
public_access_prevention: args.includes("--public-access-prevention")
|
|
82
|
+
? "enforced"
|
|
83
|
+
: "inherited",
|
|
84
|
+
lifecycle_config: null as never,
|
|
85
|
+
};
|
|
86
|
+
// GCS adds the convenience bindings to every new bucket.
|
|
87
|
+
state.bucketPolicy.push(
|
|
88
|
+
{
|
|
89
|
+
role: "roles/storage.legacyBucketOwner",
|
|
90
|
+
members: ["projectOwner:acme", "projectEditor:acme"],
|
|
91
|
+
},
|
|
92
|
+
{ role: "roles/storage.legacyBucketReader", members: ["projectViewer:acme"] },
|
|
93
|
+
);
|
|
94
|
+
return ok();
|
|
95
|
+
}
|
|
96
|
+
if (line.startsWith("storage buckets update")) {
|
|
97
|
+
if (!state.bucket) return fail("NotFound");
|
|
98
|
+
if (args.includes("--versioning")) state.bucket.versioning_enabled = true;
|
|
99
|
+
if (args.includes("--uniform-bucket-level-access"))
|
|
100
|
+
state.bucket.uniform_bucket_level_access = true;
|
|
101
|
+
if (args.includes("--public-access-prevention"))
|
|
102
|
+
state.bucket.public_access_prevention = "enforced";
|
|
103
|
+
if (flag(args, "lifecycle-file")) {
|
|
104
|
+
state.bucket.lifecycle_config = {
|
|
105
|
+
rule: [
|
|
106
|
+
{
|
|
107
|
+
action: { type: "Delete" },
|
|
108
|
+
condition: { daysSinceNoncurrentTime: NONCURRENT_VERSION_DAYS, isLive: false },
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
} as never;
|
|
112
|
+
}
|
|
113
|
+
return ok();
|
|
114
|
+
}
|
|
115
|
+
if (line.startsWith("storage buckets list")) {
|
|
116
|
+
return ok(state.bucket && !state.foreignBucket ? "acme-caelo-ops\n" : "acme-media\n");
|
|
117
|
+
}
|
|
118
|
+
if (line.startsWith("storage buckets get-iam-policy")) {
|
|
119
|
+
return ok(JSON.stringify({ bindings: state.bucketPolicy }));
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith("storage buckets add-iam-policy-binding")) {
|
|
122
|
+
bind(state.bucketPolicy, flag(args, "role") ?? "", flag(args, "member") ?? "");
|
|
123
|
+
return ok();
|
|
124
|
+
}
|
|
125
|
+
if (line.startsWith("storage buckets remove-iam-policy-binding")) {
|
|
126
|
+
const b = state.bucketPolicy.find((x) => x.role === flag(args, "role"));
|
|
127
|
+
if (b) b.members = b.members.filter((m) => m !== flag(args, "member"));
|
|
128
|
+
return ok();
|
|
129
|
+
}
|
|
130
|
+
if (line.startsWith("kms keyrings describe")) {
|
|
131
|
+
return state.ring
|
|
132
|
+
? ok("projects/acme/locations/europe-west1/keyRings/caelo")
|
|
133
|
+
: fail("NOT_FOUND: KeyRing");
|
|
134
|
+
}
|
|
135
|
+
if (line.startsWith("kms keyrings create")) {
|
|
136
|
+
state.ring = true;
|
|
137
|
+
return ok();
|
|
138
|
+
}
|
|
139
|
+
if (line.startsWith("kms keys describe")) {
|
|
140
|
+
return state.key ? ok(opsKmsKeyName("acme", "europe-west1")) : fail("NOT_FOUND: CryptoKey");
|
|
141
|
+
}
|
|
142
|
+
if (line.startsWith("kms keys create")) {
|
|
143
|
+
state.key = true;
|
|
144
|
+
return ok();
|
|
145
|
+
}
|
|
146
|
+
if (line.startsWith("kms keys get-iam-policy")) {
|
|
147
|
+
return ok(JSON.stringify({ bindings: state.keyPolicy }));
|
|
148
|
+
}
|
|
149
|
+
if (line.startsWith("kms keys add-iam-policy-binding")) {
|
|
150
|
+
bind(state.keyPolicy, flag(args, "role") ?? "", flag(args, "member") ?? "");
|
|
151
|
+
return ok();
|
|
152
|
+
}
|
|
153
|
+
return fail(`unexpected gcloud ${line}`);
|
|
154
|
+
};
|
|
155
|
+
const writes = () => calls.filter((c) => !/ (describe|get-iam-policy|list) /.test(` ${c} `));
|
|
156
|
+
return { run, calls, state, writes };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const TARGET = {
|
|
160
|
+
projectId: "acme",
|
|
161
|
+
region: "europe-west1",
|
|
162
|
+
operatorAccount: "Owner@Example.com",
|
|
163
|
+
provisionerEmail: "caelo-provisioner@acme.iam.gserviceaccount.com",
|
|
164
|
+
};
|
|
165
|
+
const OPERATOR = "user:owner@example.com";
|
|
166
|
+
const PROVISIONER = "serviceAccount:caelo-provisioner@acme.iam.gserviceaccount.com";
|
|
167
|
+
const noSleep = async () => {};
|
|
168
|
+
|
|
169
|
+
describe("ensureOpsBackend — new install", () => {
|
|
170
|
+
it("creates a hardened bucket + KMS key that only the operator and the provisioner can use", async () => {
|
|
171
|
+
const g = fakeGcp();
|
|
172
|
+
const r = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
173
|
+
expect(r.ok).toBe(true);
|
|
174
|
+
if (!r.ok) return;
|
|
175
|
+
expect(r.record).toEqual({
|
|
176
|
+
kind: "gcs",
|
|
177
|
+
bucket: "acme-caelo-ops",
|
|
178
|
+
kmsKey: "projects/acme/locations/europe-west1/keyRings/caelo/cryptoKeys/pulumi-state",
|
|
179
|
+
});
|
|
180
|
+
expect(g.state.apis.has("cloudkms.googleapis.com")).toBe(true);
|
|
181
|
+
expect(g.state.bucket).toMatchObject({
|
|
182
|
+
versioning_enabled: true,
|
|
183
|
+
uniform_bucket_level_access: true,
|
|
184
|
+
public_access_prevention: "enforced",
|
|
185
|
+
});
|
|
186
|
+
expect(
|
|
187
|
+
g.calls.some((c) => c.includes("--location=europe-west1") && c.includes("buckets create")),
|
|
188
|
+
).toBe(true);
|
|
189
|
+
// Bucket: exactly the operator + the provisioner hold object access; the
|
|
190
|
+
// viewer/editor convenience bindings are gone; nothing public.
|
|
191
|
+
const objectAdmins = g.state.bucketPolicy.find((b) => b.role === OPS_BUCKET_ROLE)?.members;
|
|
192
|
+
expect(objectAdmins?.sort()).toEqual([PROVISIONER, OPERATOR].sort());
|
|
193
|
+
const everyone = g.state.bucketPolicy.flatMap((b) => b.members);
|
|
194
|
+
expect(everyone.some((m) => /^project(Viewer|Editor):/.test(m))).toBe(false);
|
|
195
|
+
expect(everyone.some((m) => m === "allUsers" || m === "allAuthenticatedUsers")).toBe(false);
|
|
196
|
+
// Key: the same two principals, encrypt/decrypt only.
|
|
197
|
+
expect(g.state.ring && g.state.key).toBe(true);
|
|
198
|
+
expect(g.state.keyPolicy).toEqual([{ role: OPS_KEY_ROLE, members: [OPERATOR, PROVISIONER] }]);
|
|
199
|
+
// Never deletes anything.
|
|
200
|
+
expect(g.calls.some((c) => / delete /.test(` ${c} `))).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("is idempotent: a second run reads and writes nothing", async () => {
|
|
204
|
+
const g = fakeGcp();
|
|
205
|
+
await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
206
|
+
const before = g.writes().length;
|
|
207
|
+
const again = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
208
|
+
expect(again.ok && again.actions).toEqual([]);
|
|
209
|
+
expect(g.writes().length).toBe(before);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("brings an existing bucket that lost its hardening back in line", async () => {
|
|
213
|
+
const g = fakeGcp({ bucket: true, ring: true, key: true });
|
|
214
|
+
if (g.state.bucket) {
|
|
215
|
+
g.state.bucket.versioning_enabled = false;
|
|
216
|
+
g.state.bucket.public_access_prevention = "inherited";
|
|
217
|
+
}
|
|
218
|
+
const r = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
219
|
+
expect(r.ok).toBe(true);
|
|
220
|
+
expect(g.state.bucket).toMatchObject({
|
|
221
|
+
versioning_enabled: true,
|
|
222
|
+
public_access_prevention: "enforced",
|
|
223
|
+
});
|
|
224
|
+
expect(g.calls.some((c) => c.startsWith("storage buckets create"))).toBe(false);
|
|
225
|
+
expect(g.calls.some((c) => c.startsWith("kms keys create"))).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("refuses a bucket that grants public access, naming the binding", async () => {
|
|
229
|
+
const g = fakeGcp({ bucket: true, ring: true, key: true });
|
|
230
|
+
g.state.bucketPolicy.push({ role: "roles/storage.objectViewer", members: ["allUsers"] });
|
|
231
|
+
const r = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
232
|
+
expect(r.ok).toBe(false);
|
|
233
|
+
if (r.ok) return;
|
|
234
|
+
expect(r.error).toContain("allUsers (roles/storage.objectViewer)");
|
|
235
|
+
expect(r.error).toContain("must never be public");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("stops on a bucket name it cannot read (taken by another project)", async () => {
|
|
239
|
+
const g = fakeGcp();
|
|
240
|
+
const run = async (args: string[]) =>
|
|
241
|
+
args.join(" ").startsWith("storage buckets describe")
|
|
242
|
+
? fail("ERROR: 403 caller does not have storage.buckets.get access")
|
|
243
|
+
: g.run(args);
|
|
244
|
+
const r = await ensureOpsBackend(TARGET, { run, sleep: noSleep });
|
|
245
|
+
expect(r.ok).toBe(false);
|
|
246
|
+
expect(g.calls.some((c) => c.startsWith("storage buckets create"))).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("refuses a same-named bucket in another project the operator can reach", async () => {
|
|
250
|
+
const g = fakeGcp({ bucket: true, ring: true, key: true });
|
|
251
|
+
g.state.foreignBucket = true;
|
|
252
|
+
const r = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
253
|
+
expect(r.ok).toBe(false);
|
|
254
|
+
if (r.ok) return;
|
|
255
|
+
expect(r.error).toContain("does not belong to project acme");
|
|
256
|
+
expect(g.writes()).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("replaces a lifecycle that could cut the recovery window or delete live state", async () => {
|
|
260
|
+
for (const bad of [
|
|
261
|
+
{
|
|
262
|
+
rule: [
|
|
263
|
+
{ action: { type: "Delete" }, condition: { daysSinceNoncurrentTime: 1, isLive: false } },
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
rule: [
|
|
268
|
+
{
|
|
269
|
+
action: { type: "Delete" },
|
|
270
|
+
condition: { daysSinceNoncurrentTime: 30, isLive: false, matchesPrefix: ["x/"] },
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
rule: [
|
|
276
|
+
{ action: { type: "Delete" }, condition: { daysSinceNoncurrentTime: 30, isLive: false } },
|
|
277
|
+
{ action: { type: "Delete" }, condition: { age: 7 } },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
]) {
|
|
281
|
+
expect(isOpsLifecycle(bad)).toBe(false);
|
|
282
|
+
const g = fakeGcp({ bucket: true, ring: true, key: true });
|
|
283
|
+
if (g.state.bucket) g.state.bucket.lifecycle_config = bad as never;
|
|
284
|
+
await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep });
|
|
285
|
+
expect(isOpsLifecycle(g.state.bucket?.lifecycle_config)).toBe(true);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("dry run lists what it would do and writes nothing", async () => {
|
|
290
|
+
const g = fakeGcp();
|
|
291
|
+
const r = await ensureOpsBackend(TARGET, { run: g.run, sleep: noSleep, dryRun: true });
|
|
292
|
+
expect(r.ok).toBe(true);
|
|
293
|
+
if (!r.ok) return;
|
|
294
|
+
expect(r.actions.every((a) => a.startsWith("would "))).toBe(true);
|
|
295
|
+
expect(r.actions.join("\n")).toContain("would create gs://acme-caelo-ops in europe-west1");
|
|
296
|
+
expect(r.actions.join("\n")).toContain("would create KMS key caelo/pulumi-state");
|
|
297
|
+
expect(g.writes()).toEqual([]);
|
|
298
|
+
expect(r.actions.join("\n")).toContain(
|
|
299
|
+
"would enable storage.googleapis.com, cloudkms.googleapis.com",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("cost table (CLAUDE.md §11.C)", () => {
|
|
305
|
+
it("lists the state bucket + KMS key at under $0.10/mo without float noise in the total", () => {
|
|
306
|
+
const est = estimateGcpCost({
|
|
307
|
+
cloudSqlTier: "db-f1-micro",
|
|
308
|
+
cloudSqlHa: false,
|
|
309
|
+
adminMinInstances: 0,
|
|
310
|
+
gatewayMinInstances: 0,
|
|
311
|
+
wafAdaptiveProtection: false,
|
|
312
|
+
provider: "gcp-firebase",
|
|
313
|
+
region: "europe-west1",
|
|
314
|
+
});
|
|
315
|
+
const line = est.lines.find((l) => l.name.includes("ops bucket"));
|
|
316
|
+
expect(line?.monthlyUsd).toBeLessThan(0.1);
|
|
317
|
+
expect(line?.notes).toContain("< $0.10/mo");
|
|
318
|
+
expect(String(est.totalUsd)).toMatch(/^\d+(\.\d{1,2})?$/);
|
|
319
|
+
});
|
|
320
|
+
});
|