@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,254 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { enableAdminDomain } from "./admin-domain.js";
|
|
5
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
6
|
+
|
|
7
|
+
const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
|
|
8
|
+
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
9
|
+
|
|
10
|
+
/** Fake gcloud; the `beta` component is installed unless a test answers `components list`. */
|
|
11
|
+
function fakeGcloud(overrides: Record<string, GcloudResult[]>) {
|
|
12
|
+
const answers: Record<string, GcloudResult[]> = {
|
|
13
|
+
"components list": [ok("beta\n")],
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
const calls: string[][] = [];
|
|
17
|
+
const run = async (args: string[]) => {
|
|
18
|
+
calls.push(args);
|
|
19
|
+
const key = Object.keys(answers).find((k) => args.join(" ").startsWith(k));
|
|
20
|
+
return (key && answers[key]?.shift()) || ok();
|
|
21
|
+
};
|
|
22
|
+
return { run, calls };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAPPING = JSON.stringify({
|
|
26
|
+
status: { resourceRecords: [{ name: "admin", type: "CNAME", rrdata: "ghs.googlehosted.com." }] },
|
|
27
|
+
});
|
|
28
|
+
const base = { projectId: "p", region: "europe-west1", domain: "example.com", log: () => {} };
|
|
29
|
+
|
|
30
|
+
describe("enableAdminDomain", () => {
|
|
31
|
+
it("creates the mapping as the operator once the domain is verified, and prints the record", async () => {
|
|
32
|
+
const { run, calls } = fakeGcloud({
|
|
33
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
34
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND"), ok(MAPPING)],
|
|
35
|
+
"domains list-user-verified": [ok("example.com\n")],
|
|
36
|
+
});
|
|
37
|
+
const r = await enableAdminDomain({ ...base, run });
|
|
38
|
+
expect(r).toEqual({
|
|
39
|
+
status: "mapped",
|
|
40
|
+
hostname: "admin.example.com",
|
|
41
|
+
created: true,
|
|
42
|
+
records: [{ name: "admin.example.com", type: "CNAME", value: "ghs.googlehosted.com." }],
|
|
43
|
+
});
|
|
44
|
+
expect(calls).toContainEqual([
|
|
45
|
+
"beta",
|
|
46
|
+
"run",
|
|
47
|
+
"domain-mappings",
|
|
48
|
+
"create",
|
|
49
|
+
"--service=caelo-production-admin-abc",
|
|
50
|
+
"--domain=admin.example.com",
|
|
51
|
+
"--region=europe-west1",
|
|
52
|
+
"--project=p",
|
|
53
|
+
"--quiet",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is idempotent: an existing mapping is reported, not recreated", async () => {
|
|
58
|
+
const { run, calls } = fakeGcloud({
|
|
59
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
60
|
+
"beta run domain-mappings describe": [ok(MAPPING)],
|
|
61
|
+
});
|
|
62
|
+
const r = await enableAdminDomain({ ...base, run });
|
|
63
|
+
expect(r.status === "mapped" && !r.created).toBe(true);
|
|
64
|
+
expect(calls.some((c) => c.includes("create"))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("starts Search Console verification and stops when the operator has not verified the domain", async () => {
|
|
68
|
+
const { run, calls } = fakeGcloud({
|
|
69
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
70
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND")],
|
|
71
|
+
"domains list-user-verified": [ok("other.org\n")],
|
|
72
|
+
});
|
|
73
|
+
const r = await enableAdminDomain({ ...base, run });
|
|
74
|
+
expect(r).toEqual({
|
|
75
|
+
status: "needs-verification",
|
|
76
|
+
hostname: "admin.example.com",
|
|
77
|
+
verifyDomain: "example.com",
|
|
78
|
+
});
|
|
79
|
+
expect(calls).toContainEqual(["domains", "verify", "example.com"]);
|
|
80
|
+
expect(calls.some((c) => c.includes("create"))).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("refuses to pick when more than one admin service matches", async () => {
|
|
84
|
+
const { run, calls } = fakeGcloud({
|
|
85
|
+
"run services list": [ok("caelo-production-admin-abc\ncaelo-production-admin-old\n")],
|
|
86
|
+
});
|
|
87
|
+
const r = await enableAdminDomain({ ...base, run });
|
|
88
|
+
expect(r.status === "failed" && r.error).toContain("exactly one");
|
|
89
|
+
expect(calls.some((c) => c.includes("create"))).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("fails (not needs-verification) when gcloud cannot list or start verification", async () => {
|
|
93
|
+
const listFails = fakeGcloud({
|
|
94
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
95
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND")],
|
|
96
|
+
"domains list-user-verified": [fail("Reauthentication required")],
|
|
97
|
+
});
|
|
98
|
+
expect(await enableAdminDomain({ ...base, run: listFails.run })).toEqual({
|
|
99
|
+
status: "failed",
|
|
100
|
+
error: "list verified domains: Reauthentication required",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const verifyFails = fakeGcloud({
|
|
104
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
105
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND")],
|
|
106
|
+
"domains list-user-verified": [ok("other.org\n")],
|
|
107
|
+
"domains verify": [fail("could not open a browser")],
|
|
108
|
+
});
|
|
109
|
+
expect(await enableAdminDomain({ ...base, run: verifyFails.run })).toEqual({
|
|
110
|
+
status: "failed",
|
|
111
|
+
error: "start domain verification for example.com: could not open a browser",
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("fails loudly when the admin service cannot be found or the create is refused", async () => {
|
|
116
|
+
const missing = fakeGcloud({ "run services list": [ok("")] });
|
|
117
|
+
expect((await enableAdminDomain({ ...base, run: missing.run })).status).toBe("failed");
|
|
118
|
+
|
|
119
|
+
const refused = fakeGcloud({
|
|
120
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
121
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND")],
|
|
122
|
+
"domains list-user-verified": [ok("admin.example.com\n")],
|
|
123
|
+
"beta run domain-mappings create": [
|
|
124
|
+
fail("Caller is not authorized to administer the domain"),
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
expect(await enableAdminDomain({ ...base, run: refused.run })).toEqual({
|
|
128
|
+
status: "failed",
|
|
129
|
+
error: "create domain mapping: Caller is not authorized to administer the domain",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("installs the missing beta component before the first beta command (regression: hidden gcloud prompt hung the CLI)", async () => {
|
|
134
|
+
const logs: string[] = [];
|
|
135
|
+
const { run, calls } = fakeGcloud({
|
|
136
|
+
"components list": [ok("")],
|
|
137
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
138
|
+
"beta run domain-mappings describe": [ok(MAPPING)],
|
|
139
|
+
});
|
|
140
|
+
const r = await enableAdminDomain({ ...base, run, log: (m) => logs.push(m) });
|
|
141
|
+
expect(r.status).toBe("mapped");
|
|
142
|
+
const install = calls.findIndex((c) => c.join(" ") === "components install beta --quiet");
|
|
143
|
+
const firstBeta = calls.findIndex((c) => c[0] === "beta");
|
|
144
|
+
expect(install).toBeGreaterThanOrEqual(0);
|
|
145
|
+
expect(install).toBeLessThan(firstBeta);
|
|
146
|
+
expect(logs.join("\n")).toContain('Installing the gcloud "beta" component');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("fails with the package-manager command when gcloud cannot install the beta component", async () => {
|
|
150
|
+
const { run, calls } = fakeGcloud({
|
|
151
|
+
"components list": [ok("")],
|
|
152
|
+
"run services list": [ok("caelo-production-admin-abc\n")],
|
|
153
|
+
"components install": [
|
|
154
|
+
fail(
|
|
155
|
+
"ERROR: (gcloud.components.install)\nYou cannot perform this action because the Google Cloud CLI component manager \nis disabled for this installation. You can run the following command \nto achieve the same result for this installation: \n\nsudo apt-get install google-cloud-cli",
|
|
156
|
+
),
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
const r = await enableAdminDomain({ ...base, run });
|
|
160
|
+
expect(r.status).toBe("failed");
|
|
161
|
+
expect(r.status === "failed" && r.error).toContain("sudo apt-get install google-cloud-cli");
|
|
162
|
+
expect(calls.some((c) => c[0] === "beta")).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("enableStagingDomain (#650)", () => {
|
|
167
|
+
const STAGING_MAPPING = JSON.stringify({
|
|
168
|
+
status: {
|
|
169
|
+
resourceRecords: [{ name: "staging", type: "CNAME", rrdata: "ghs.googlehosted.com." }],
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("opens the gateway's ingress, maps staging.<domain> to the gateway and prints the record", async () => {
|
|
174
|
+
const { run, calls } = fakeGcloud({
|
|
175
|
+
"run services list": [ok("caelo-production-gateway-xyz\n")],
|
|
176
|
+
"run services describe": [ok("internal-and-cloud-load-balancing\n")],
|
|
177
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND"), ok(STAGING_MAPPING)],
|
|
178
|
+
"domains list-user-verified": [ok("example.com\n")],
|
|
179
|
+
});
|
|
180
|
+
const { enableStagingDomain } = await import("./admin-domain.js");
|
|
181
|
+
const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
|
|
182
|
+
expect(r).toEqual({
|
|
183
|
+
status: "mapped",
|
|
184
|
+
hostname: "staging.example.com",
|
|
185
|
+
created: true,
|
|
186
|
+
records: [{ name: "staging.example.com", type: "CNAME", value: "ghs.googlehosted.com." }],
|
|
187
|
+
});
|
|
188
|
+
expect(calls).toContainEqual([
|
|
189
|
+
"run",
|
|
190
|
+
"services",
|
|
191
|
+
"update",
|
|
192
|
+
"caelo-production-gateway-xyz",
|
|
193
|
+
"--region=europe-west1",
|
|
194
|
+
"--project=p",
|
|
195
|
+
"--ingress=all",
|
|
196
|
+
"--quiet",
|
|
197
|
+
]);
|
|
198
|
+
expect(calls.find((c) => c.includes("create"))).toContain(
|
|
199
|
+
"--service=caelo-production-gateway-xyz",
|
|
200
|
+
);
|
|
201
|
+
expect(calls[0]).toContain("--filter=metadata.name~^caelo-production-gateway");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("is idempotent: ingress already open and mapping present ⇒ no update, no create", async () => {
|
|
205
|
+
const { run, calls } = fakeGcloud({
|
|
206
|
+
"run services list": [ok("caelo-production-gateway-xyz\n")],
|
|
207
|
+
"run services describe": [ok("all\n")],
|
|
208
|
+
"beta run domain-mappings describe": [ok(STAGING_MAPPING)],
|
|
209
|
+
});
|
|
210
|
+
const { enableStagingDomain } = await import("./admin-domain.js");
|
|
211
|
+
const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
|
|
212
|
+
expect(r.status === "mapped" && !r.created).toBe(true);
|
|
213
|
+
expect(calls.some((c) => c.includes("update") || c.includes("create"))).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("maps a host the operator chose on another domain, verifying that host itself", async () => {
|
|
217
|
+
const { run, calls } = fakeGcloud({
|
|
218
|
+
"run services list": [ok("caelo-production-gateway-xyz\n")],
|
|
219
|
+
"run services describe": [ok("all\n")],
|
|
220
|
+
"beta run domain-mappings describe": [fail("NOT_FOUND")],
|
|
221
|
+
"domains list-user-verified": [ok("example.com\n")],
|
|
222
|
+
});
|
|
223
|
+
const { enableStagingDomain } = await import("./admin-domain.js");
|
|
224
|
+
const r = await enableStagingDomain({ ...base, stagingHost: "preview.other.de", run });
|
|
225
|
+
expect(r).toEqual({
|
|
226
|
+
status: "needs-verification",
|
|
227
|
+
hostname: "preview.other.de",
|
|
228
|
+
verifyDomain: "preview.other.de",
|
|
229
|
+
});
|
|
230
|
+
expect(calls).toContainEqual(["domains", "verify", "preview.other.de"]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("fails closed without exactly one gateway service", async () => {
|
|
234
|
+
const { run } = fakeGcloud({ "run services list": [ok("a\nb\n")] });
|
|
235
|
+
const { enableStagingDomain } = await import("./admin-domain.js");
|
|
236
|
+
const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
|
|
237
|
+
expect(r.status).toBe("failed");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("installs the missing beta component before its first beta command", async () => {
|
|
241
|
+
const { run, calls } = fakeGcloud({
|
|
242
|
+
"components list": [ok("")],
|
|
243
|
+
"run services list": [ok("caelo-production-gateway-xyz\n")],
|
|
244
|
+
"run services describe": [ok("all\n")],
|
|
245
|
+
"beta run domain-mappings describe": [ok(STAGING_MAPPING)],
|
|
246
|
+
});
|
|
247
|
+
const { enableStagingDomain } = await import("./admin-domain.js");
|
|
248
|
+
const r = await enableStagingDomain({ ...base, stagingHost: "staging.example.com", run });
|
|
249
|
+
expect(r.status).toBe("mapped");
|
|
250
|
+
const install = calls.findIndex((c) => c.join(" ") === "components install beta --quiet");
|
|
251
|
+
expect(install).toBeGreaterThanOrEqual(0);
|
|
252
|
+
expect(install).toBeLessThan(calls.findIndex((c) => c[0] === "beta"));
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `cms-provision admin-domain enable` — serve a gcp-firebase admin at
|
|
5
|
+
* `admin.<domain>` instead of its `*.run.app` URL — and, the same way,
|
|
6
|
+
* `cms-provision staging-domain enable` (#650): serve the protected staging
|
|
7
|
+
* site at `staging.<domain>` from the gateway, which gates and proxies it.
|
|
8
|
+
*
|
|
9
|
+
* The gcp-firebase stack only creates that Cloud Run domain mapping behind
|
|
10
|
+
* `provisionAdminDomain=true`, off by default: Google refuses a mapping
|
|
11
|
+
* unless the CALLER is a verified owner of the domain in Search Console, and
|
|
12
|
+
* the stack runs as the provisioner service account, which never is. So the
|
|
13
|
+
* mapping is created here, as the operator's own gcloud identity — the one
|
|
14
|
+
* that verified (or now verifies) the domain:
|
|
15
|
+
*
|
|
16
|
+
* 1. find the admin Cloud Run service (`caelo-production-admin*`);
|
|
17
|
+
* 2. if the mapping exists already, just report its DNS records;
|
|
18
|
+
* 3. check `gcloud domains list-user-verified`; if neither the apex nor
|
|
19
|
+
* `admin.<domain>` is verified, start `gcloud domains verify <apex>`
|
|
20
|
+
* (opens Search Console) and stop — verifying means adding a TXT record,
|
|
21
|
+
* the one step only the domain's owner can take;
|
|
22
|
+
* 4. create the mapping and print the DNS records to add.
|
|
23
|
+
*
|
|
24
|
+
* The mapping is then owned by this command, not by Pulumi: leave
|
|
25
|
+
* `provisionAdminDomain` false, or the next `pulumi up` tries to create a
|
|
26
|
+
* second one and fails with "already exists".
|
|
27
|
+
*
|
|
28
|
+
* `gcp` installs need none of this — the load balancer serves admin.<domain>
|
|
29
|
+
* from the start.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
|
|
33
|
+
import { ensureGcloudComponent } from "./gcloud-components.js";
|
|
34
|
+
|
|
35
|
+
type Run = (args: string[]) => Promise<GcloudResult>;
|
|
36
|
+
|
|
37
|
+
export interface DnsRecordToAdd {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly type: string;
|
|
40
|
+
readonly value: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type AdminDomainResult =
|
|
44
|
+
| { status: "mapped"; hostname: string; created: boolean; records: DnsRecordToAdd[] }
|
|
45
|
+
| { status: "needs-verification"; hostname: string; verifyDomain: string }
|
|
46
|
+
| { status: "failed"; error: string };
|
|
47
|
+
|
|
48
|
+
/** What `staging-domain enable` reports; same shape as the admin's. */
|
|
49
|
+
export type StagingDomainResult = AdminDomainResult;
|
|
50
|
+
|
|
51
|
+
interface DomainMappingJson {
|
|
52
|
+
status?: { resourceRecords?: { name?: string; type?: string; rrdata?: string }[] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function recordsOf(json: string, hostname: string): DnsRecordToAdd[] {
|
|
56
|
+
const parsed = JSON.parse(json) as DomainMappingJson;
|
|
57
|
+
return (parsed.status?.resourceRecords ?? []).map((r) => ({
|
|
58
|
+
name: r.name ? `${r.name}.${hostname.split(".").slice(1).join(".")}` : hostname,
|
|
59
|
+
type: r.type ?? "CNAME",
|
|
60
|
+
value: r.rrdata ?? "",
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface DomainOpts {
|
|
65
|
+
projectId: string;
|
|
66
|
+
region: string;
|
|
67
|
+
domain: string;
|
|
68
|
+
run?: Run;
|
|
69
|
+
/** Progress lines (the one-time `beta` component install); defaults to stdout. */
|
|
70
|
+
log?: (message: string) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logOf(opts: DomainOpts): (message: string) => void {
|
|
74
|
+
return opts.log ?? ((message: string) => console.log(message));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The one deployed Cloud Run service of `slug` (Pulumi-suffixed name); also used by staging-admin.ts. */
|
|
78
|
+
export async function findService(
|
|
79
|
+
run: Run,
|
|
80
|
+
scope: string[],
|
|
81
|
+
slug: "admin" | "gateway",
|
|
82
|
+
): Promise<{ ok: true; service: string } | { ok: false; error: string }> {
|
|
83
|
+
const prefix = `caelo-production-${slug}`;
|
|
84
|
+
const services = await run([
|
|
85
|
+
"run",
|
|
86
|
+
"services",
|
|
87
|
+
"list",
|
|
88
|
+
...scope,
|
|
89
|
+
`--filter=metadata.name~^${prefix}`,
|
|
90
|
+
"--format=value(metadata.name)",
|
|
91
|
+
]);
|
|
92
|
+
if (!services.ok) {
|
|
93
|
+
return { ok: false, error: `list Cloud Run services: ${services.stderr.trim()}` };
|
|
94
|
+
}
|
|
95
|
+
const matches = services.stdout
|
|
96
|
+
.split("\n")
|
|
97
|
+
.map((s) => s.trim())
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
// Fail closed like the other service lookups: with a leftover service from
|
|
100
|
+
// a failed deploy, picking one would bind the domain to the wrong revision.
|
|
101
|
+
const service = matches.length === 1 ? matches[0] : undefined;
|
|
102
|
+
if (!service) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error:
|
|
106
|
+
matches.length === 0
|
|
107
|
+
? `${slug} Cloud Run service not found: no ${prefix}* service`
|
|
108
|
+
: `expected exactly one ${prefix}* Cloud Run service, found ${matches.length} (${matches.join(", ")}); delete the stale one first`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, service };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function enableAdminDomain(opts: DomainOpts): Promise<AdminDomainResult> {
|
|
115
|
+
const run = opts.run ?? defaultGcloud;
|
|
116
|
+
const scope = [`--region=${opts.region}`, `--project=${opts.projectId}`];
|
|
117
|
+
const found = await findService(run, scope, "admin");
|
|
118
|
+
if (!found.ok) return { status: "failed", error: found.error };
|
|
119
|
+
return mapDomain(run, logOf(opts), scope, opts.domain, `admin.${opts.domain}`, found.service);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* #650 — map the install's staging host (its setting, default
|
|
124
|
+
* `staging.<domain>`; any FQDN) to the gateway, which serves the protected
|
|
125
|
+
* staging site. Cloud Run refuses domain-mapped requests while a service's
|
|
126
|
+
* ingress is internal-only, so the gateway's ingress is opened first (the
|
|
127
|
+
* stack declares the same since #650; installs from before keep the old
|
|
128
|
+
* value until this runs). Idempotent like the admin mapping.
|
|
129
|
+
*/
|
|
130
|
+
export async function enableStagingDomain(
|
|
131
|
+
opts: DomainOpts & { stagingHost: string },
|
|
132
|
+
): Promise<StagingDomainResult> {
|
|
133
|
+
const run = opts.run ?? defaultGcloud;
|
|
134
|
+
const scope = [`--region=${opts.region}`, `--project=${opts.projectId}`];
|
|
135
|
+
const found = await findService(run, scope, "gateway");
|
|
136
|
+
if (!found.ok) return { status: "failed", error: found.error };
|
|
137
|
+
const ingress = await run([
|
|
138
|
+
"run",
|
|
139
|
+
"services",
|
|
140
|
+
"describe",
|
|
141
|
+
found.service,
|
|
142
|
+
...scope,
|
|
143
|
+
'--format=value(metadata.annotations["run.googleapis.com/ingress"])',
|
|
144
|
+
]);
|
|
145
|
+
if (!ingress.ok) {
|
|
146
|
+
return { status: "failed", error: `read the gateway's ingress: ${ingress.stderr.trim()}` };
|
|
147
|
+
}
|
|
148
|
+
if (ingress.stdout.trim() !== "all") {
|
|
149
|
+
const update = await run([
|
|
150
|
+
"run",
|
|
151
|
+
"services",
|
|
152
|
+
"update",
|
|
153
|
+
found.service,
|
|
154
|
+
...scope,
|
|
155
|
+
"--ingress=all",
|
|
156
|
+
"--quiet",
|
|
157
|
+
]);
|
|
158
|
+
if (!update.ok) {
|
|
159
|
+
return {
|
|
160
|
+
status: "failed",
|
|
161
|
+
error: `open the gateway's ingress for the domain mapping: ${update.stderr.trim()}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Search Console verification: the install domain covers its own
|
|
166
|
+
// subdomains; a host on another domain is verified itself.
|
|
167
|
+
const verifyDomain =
|
|
168
|
+
opts.stagingHost === opts.domain || opts.stagingHost.endsWith(`.${opts.domain}`)
|
|
169
|
+
? opts.domain
|
|
170
|
+
: opts.stagingHost;
|
|
171
|
+
return mapDomain(run, logOf(opts), scope, verifyDomain, opts.stagingHost, found.service);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Steps 2–4 of the module doc for `hostname` → `service`. */
|
|
175
|
+
async function mapDomain(
|
|
176
|
+
run: Run,
|
|
177
|
+
log: (message: string) => void,
|
|
178
|
+
scope: string[],
|
|
179
|
+
domain: string,
|
|
180
|
+
hostname: string,
|
|
181
|
+
service: string,
|
|
182
|
+
): Promise<AdminDomainResult> {
|
|
183
|
+
// Fully managed Cloud Run domain mappings exist only on the beta track: the
|
|
184
|
+
// GA `gcloud run domain-mappings` is Cloud Run for Anthos only. Install the
|
|
185
|
+
// component up front instead of letting gcloud ask for it mid-command.
|
|
186
|
+
const beta = await ensureGcloudComponent("beta", { run, log });
|
|
187
|
+
if (!beta.ok) return { status: "failed", error: beta.error };
|
|
188
|
+
|
|
189
|
+
const describeArgs = [
|
|
190
|
+
"beta",
|
|
191
|
+
"run",
|
|
192
|
+
"domain-mappings",
|
|
193
|
+
"describe",
|
|
194
|
+
`--domain=${hostname}`,
|
|
195
|
+
...scope,
|
|
196
|
+
"--format=json",
|
|
197
|
+
];
|
|
198
|
+
const existing = await run(describeArgs);
|
|
199
|
+
if (existing.ok) {
|
|
200
|
+
return {
|
|
201
|
+
status: "mapped",
|
|
202
|
+
hostname,
|
|
203
|
+
created: false,
|
|
204
|
+
records: recordsOf(existing.stdout, hostname),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const verified = await run(["domains", "list-user-verified", "--format=value(id)"]);
|
|
209
|
+
if (!verified.ok) {
|
|
210
|
+
// Expired credentials or a missing gcloud component — not "unverified".
|
|
211
|
+
return { status: "failed", error: `list verified domains: ${verified.stderr.trim()}` };
|
|
212
|
+
}
|
|
213
|
+
const verifiedIds = verified.stdout.split("\n").map((s) => s.trim());
|
|
214
|
+
if (!verifiedIds.includes(domain) && !verifiedIds.includes(hostname)) {
|
|
215
|
+
const verify = await run(["domains", "verify", domain]);
|
|
216
|
+
if (!verify.ok) {
|
|
217
|
+
return {
|
|
218
|
+
status: "failed",
|
|
219
|
+
error: `start domain verification for ${domain}: ${verify.stderr.trim()}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { status: "needs-verification", hostname, verifyDomain: domain };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const create = await run([
|
|
226
|
+
"beta",
|
|
227
|
+
"run",
|
|
228
|
+
"domain-mappings",
|
|
229
|
+
"create",
|
|
230
|
+
`--service=${service}`,
|
|
231
|
+
`--domain=${hostname}`,
|
|
232
|
+
...scope,
|
|
233
|
+
"--quiet",
|
|
234
|
+
]);
|
|
235
|
+
if (!create.ok) {
|
|
236
|
+
return { status: "failed", error: `create domain mapping: ${create.stderr.trim()}` };
|
|
237
|
+
}
|
|
238
|
+
const created = await run(describeArgs);
|
|
239
|
+
return {
|
|
240
|
+
status: "mapped",
|
|
241
|
+
hostname,
|
|
242
|
+
created: true,
|
|
243
|
+
records: created.ok ? recordsOf(created.stdout, hostname) : [],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #553 — the admin memory knob (it runs the Lighthouse quality audit):
|
|
5
|
+
* one default for every adapter, upgrade raises existing installs to it
|
|
6
|
+
* (never lowers), and the cost table prices it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { generateDockerCompose } from "./compose.js";
|
|
11
|
+
import {
|
|
12
|
+
ADMIN_MEMORY_DEFAULT,
|
|
13
|
+
azureContainerAppResources,
|
|
14
|
+
memoryQuantityMiB,
|
|
15
|
+
} from "./stack-contract.js";
|
|
16
|
+
import { liveContainerMemory, planAdminMemory, serviceRollArgs } from "./stack-converge.js";
|
|
17
|
+
import { estimateGcpCost } from "./wizards/gcp-cost.js";
|
|
18
|
+
|
|
19
|
+
describe("memoryQuantityMiB", () => {
|
|
20
|
+
it("parses the notations the stacks and gcloud use", () => {
|
|
21
|
+
expect(memoryQuantityMiB("512Mi")).toBe(512);
|
|
22
|
+
expect(memoryQuantityMiB("2Gi")).toBe(2048);
|
|
23
|
+
expect(memoryQuantityMiB("1.5Gi")).toBe(1536);
|
|
24
|
+
expect(memoryQuantityMiB("2G")).toBe(1907);
|
|
25
|
+
expect(memoryQuantityMiB("2 GB")).toBeNull();
|
|
26
|
+
expect(memoryQuantityMiB("lots")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("the default is 2Gi", () => {
|
|
30
|
+
expect(ADMIN_MEMORY_DEFAULT).toBe("2Gi");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("planAdminMemory (upgrade)", () => {
|
|
35
|
+
it("raises an admin below the default, including Cloud Run's implicit 512Mi", () => {
|
|
36
|
+
expect(planAdminMemory("1Gi")).toEqual({ ok: true, flags: ["--memory=2Gi"], from: "1Gi" });
|
|
37
|
+
expect(planAdminMemory(null)).toEqual({ ok: true, flags: ["--memory=2Gi"], from: null });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("never lowers an operator's larger value", () => {
|
|
41
|
+
expect(planAdminMemory("4Gi")).toEqual({ ok: true, flags: [], from: "4Gi" });
|
|
42
|
+
expect(planAdminMemory("2Gi")).toEqual({ ok: true, flags: [], from: "2Gi" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("leaves a value it cannot parse alone, with a reason", () => {
|
|
46
|
+
const r = planAdminMemory("lots");
|
|
47
|
+
expect(r.ok).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reads the live limit from `services describe` JSON", () => {
|
|
51
|
+
const json = JSON.stringify({
|
|
52
|
+
spec: { template: { spec: { containers: [{ resources: { limits: { memory: "1Gi" } } }] } } },
|
|
53
|
+
});
|
|
54
|
+
expect(liveContainerMemory(json)).toBe("1Gi");
|
|
55
|
+
expect(liveContainerMemory(JSON.stringify({ spec: {} }))).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rides the same `services update` as the image roll", () => {
|
|
59
|
+
const args = serviceRollArgs({
|
|
60
|
+
serviceName: "caelo-production-admin",
|
|
61
|
+
region: "europe-west1",
|
|
62
|
+
projectId: "acme",
|
|
63
|
+
imageRef: "img@sha256:abc",
|
|
64
|
+
serviceAccount: "sa@acme.iam.gserviceaccount.com",
|
|
65
|
+
envFlags: ["--update-env-vars=A=b"],
|
|
66
|
+
resourceFlags: ["--memory=2Gi"],
|
|
67
|
+
});
|
|
68
|
+
expect(args).toContain("--memory=2Gi");
|
|
69
|
+
expect(args.indexOf("--memory=2Gi")).toBeLessThan(args.indexOf("--quiet"));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("self-hosted compose", () => {
|
|
74
|
+
const base = {
|
|
75
|
+
domain: "acme.com",
|
|
76
|
+
postgresPassword: "pw",
|
|
77
|
+
rolePasswords: { admin: "a", public: "b", gateway: "g" },
|
|
78
|
+
minioRootUser: "caelo",
|
|
79
|
+
minioRootPassword: "pw",
|
|
80
|
+
caeloSecretKek: "0".repeat(64),
|
|
81
|
+
diskSize: "20Gi",
|
|
82
|
+
images: {
|
|
83
|
+
mode: "release" as const,
|
|
84
|
+
admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
|
|
85
|
+
gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
it("reserves the default admin memory, or the configured one", () => {
|
|
90
|
+
expect(generateDockerCompose(base)).toContain("mem_reservation: 2048m");
|
|
91
|
+
expect(generateDockerCompose({ ...base, adminMemory: "3Gi" })).toContain(
|
|
92
|
+
"mem_reservation: 3072m",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("refuses a value that is not a memory quantity", () => {
|
|
97
|
+
expect(() => generateDockerCompose({ ...base, adminMemory: "plenty" })).toThrow(
|
|
98
|
+
/not a memory quantity/,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("GCP cost table", () => {
|
|
104
|
+
const inputs = {
|
|
105
|
+
cloudSqlTier: "db-f1-micro",
|
|
106
|
+
cloudSqlHa: false,
|
|
107
|
+
adminMinInstances: 0,
|
|
108
|
+
gatewayMinInstances: 0,
|
|
109
|
+
wafAdaptiveProtection: false,
|
|
110
|
+
region: "europe-west1",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
it("shows the admin memory and a line for the quality checks", () => {
|
|
114
|
+
const { lines } = estimateGcpCost(inputs);
|
|
115
|
+
expect(lines.map((l) => l.name)).toContain("Cloud Run admin (2Gi)");
|
|
116
|
+
expect(lines.some((l) => l.name.startsWith("Quality checks"))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("prices the extra memory of always-on admin instances", () => {
|
|
120
|
+
const admin = (memory: string) =>
|
|
121
|
+
estimateGcpCost({ ...inputs, adminMinInstances: 1, adminMemory: memory }).lines.find((l) =>
|
|
122
|
+
l.name.startsWith("Cloud Run admin"),
|
|
123
|
+
)?.monthlyUsd;
|
|
124
|
+
expect(admin("1Gi")).toBe(16);
|
|
125
|
+
expect(admin("2Gi")).toBe(23);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("refuses a memory value provisioning would refuse", () => {
|
|
129
|
+
expect(() => estimateGcpCost({ ...inputs, adminMemory: "lots" })).toThrow("adminMemory");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("shows the install region, and flags rates priced for another region (#607)", () => {
|
|
133
|
+
expect(estimateGcpCost(inputs).regionNote).toBe("Region: europe-west1 (fixed after install)");
|
|
134
|
+
const other = estimateGcpCost({ ...inputs, region: "us-east4" }).regionNote;
|
|
135
|
+
expect(other).toContain("Region: us-east4");
|
|
136
|
+
expect(other).toContain("europe-west1 list rates");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Azure Container Apps sizes", () => {
|
|
141
|
+
it("pairs the memory with its vCPU", () => {
|
|
142
|
+
expect(azureContainerAppResources("2Gi")).toEqual({ cpu: 1, memory: "2.0Gi" });
|
|
143
|
+
expect(azureContainerAppResources("1536Mi")).toEqual({ cpu: 0.75, memory: "1.5Gi" });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("refuses sizes Container Apps does not offer", () => {
|
|
147
|
+
for (const q of ["2G", "3.2Gi", "16Gi", "256Mi", "lots"]) {
|
|
148
|
+
expect(() => azureContainerAppResources(q)).toThrow("0.5Gi to 8Gi");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|