@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
package/src/cert-wait.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Waiting for a Google-managed TLS certificate (§11.C: the CLI polls certs
|
|
5
|
+
* itself instead of telling the operator to check back later).
|
|
6
|
+
*
|
|
7
|
+
* Two certificate sources, one wait loop:
|
|
8
|
+
* - the `gcp` load balancer's managed SSL certificates
|
|
9
|
+
* ({@link lbCertProbe}) — the installer waits for the admin host;
|
|
10
|
+
* - a Cloud Run domain mapping's certificate ({@link domainMappingProbe})
|
|
11
|
+
* — `admin-domain enable` / `staging-domain enable` on gcp-firebase.
|
|
12
|
+
*
|
|
13
|
+
* {@link waitForCert} is pure apart from its injected clock, sleep and
|
|
14
|
+
* probe, so the timeout and progress behaviour is unit-tested without
|
|
15
|
+
* gcloud or real time.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
19
|
+
|
|
20
|
+
type Run = (args: string[]) => Promise<GcloudResult>;
|
|
21
|
+
|
|
22
|
+
/** One look at a certificate. */
|
|
23
|
+
export type CertProbe =
|
|
24
|
+
| { readonly state: "ready" }
|
|
25
|
+
| { readonly state: "pending"; readonly detail: string }
|
|
26
|
+
/** gcloud itself failed (auth, permissions) — waiting will not fix it. */
|
|
27
|
+
| { readonly state: "error"; readonly error: string };
|
|
28
|
+
|
|
29
|
+
export type CertWaitResult =
|
|
30
|
+
| { readonly ok: true }
|
|
31
|
+
| { readonly ok: false; readonly reason: "timeout"; readonly lastDetail: string }
|
|
32
|
+
| { readonly ok: false; readonly reason: "error"; readonly error: string };
|
|
33
|
+
|
|
34
|
+
export interface CertWaitOpts {
|
|
35
|
+
readonly probe: () => Promise<CertProbe>;
|
|
36
|
+
readonly timeoutMs: number;
|
|
37
|
+
readonly intervalMs: number;
|
|
38
|
+
/** Called whenever the probe's detail text changes (spinner message). */
|
|
39
|
+
readonly onUpdate?: (detail: string) => void;
|
|
40
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
41
|
+
readonly now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Poll `probe` until the certificate is ready, gcloud fails, or `timeoutMs`
|
|
46
|
+
* passes. The probe runs once more at the deadline so a certificate that
|
|
47
|
+
* flips during the last interval is not reported as a timeout.
|
|
48
|
+
*/
|
|
49
|
+
export async function waitForCert(opts: CertWaitOpts): Promise<CertWaitResult> {
|
|
50
|
+
const now = opts.now ?? Date.now;
|
|
51
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
52
|
+
const deadline = now() + opts.timeoutMs;
|
|
53
|
+
let lastDetail = "";
|
|
54
|
+
for (;;) {
|
|
55
|
+
const p = await opts.probe();
|
|
56
|
+
if (p.state === "ready") return { ok: true };
|
|
57
|
+
if (p.state === "error") return { ok: false, reason: "error", error: p.error };
|
|
58
|
+
if (p.detail !== lastDetail) {
|
|
59
|
+
lastDetail = p.detail;
|
|
60
|
+
opts.onUpdate?.(p.detail);
|
|
61
|
+
}
|
|
62
|
+
const remaining = deadline - now();
|
|
63
|
+
if (remaining <= 0) return { ok: false, reason: "timeout", lastDetail };
|
|
64
|
+
await sleep(Math.min(opts.intervalMs, remaining));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface MappingCondition {
|
|
69
|
+
type?: string;
|
|
70
|
+
status?: string;
|
|
71
|
+
message?: string;
|
|
72
|
+
reason?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The certificate state of the Cloud Run domain mapping for `hostname`.
|
|
77
|
+
* Ready once the mapping's `Ready` condition is True (Google sets it after
|
|
78
|
+
* `CertificateProvisioned` and `DomainRoutable`); until then the detail is
|
|
79
|
+
* the first non-True condition's message, which says what Google waits for
|
|
80
|
+
* (usually the DNS record).
|
|
81
|
+
*/
|
|
82
|
+
export async function domainMappingProbe(
|
|
83
|
+
run: Run,
|
|
84
|
+
target: { readonly hostname: string; readonly region: string; readonly projectId: string },
|
|
85
|
+
): Promise<CertProbe> {
|
|
86
|
+
const r = await run([
|
|
87
|
+
"beta",
|
|
88
|
+
"run",
|
|
89
|
+
"domain-mappings",
|
|
90
|
+
"describe",
|
|
91
|
+
`--domain=${target.hostname}`,
|
|
92
|
+
`--region=${target.region}`,
|
|
93
|
+
`--project=${target.projectId}`,
|
|
94
|
+
"--format=json",
|
|
95
|
+
]);
|
|
96
|
+
if (!r.ok) return { state: "error", error: `describe the domain mapping: ${r.stderr.trim()}` };
|
|
97
|
+
const conditions =
|
|
98
|
+
(JSON.parse(r.stdout) as { status?: { conditions?: MappingCondition[] } }).status?.conditions ??
|
|
99
|
+
[];
|
|
100
|
+
const ready = conditions.find((c) => c.type === "Ready");
|
|
101
|
+
if (ready?.status === "True") return { state: "ready" };
|
|
102
|
+
const blocking =
|
|
103
|
+
conditions.find((c) => c.type === "CertificateProvisioned" && c.status !== "True") ??
|
|
104
|
+
conditions.find((c) => c.status !== "True");
|
|
105
|
+
const detail = blocking
|
|
106
|
+
? `${blocking.type}: ${blocking.message || blocking.reason || blocking.status || "pending"}`
|
|
107
|
+
: "Google has not reported the mapping's status yet";
|
|
108
|
+
return { state: "pending", detail };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface SslCertificateJson {
|
|
112
|
+
name?: string;
|
|
113
|
+
managed?: { status?: string; domainStatus?: Record<string, string> };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the `gcp` load balancer has an ACTIVE managed certificate that
|
|
118
|
+
* covers `hostname`. Any such certificate counts: installs from before the
|
|
119
|
+
* admin got its own certificate serve the admin from the combined one.
|
|
120
|
+
* A certificate is only served once its overall status is ACTIVE, so a
|
|
121
|
+
* combined certificate whose other domain still fails does not count.
|
|
122
|
+
*/
|
|
123
|
+
export async function lbCertProbe(
|
|
124
|
+
run: Run,
|
|
125
|
+
target: { readonly projectId: string; readonly hostname: string },
|
|
126
|
+
): Promise<CertProbe> {
|
|
127
|
+
const r = await run([
|
|
128
|
+
"compute",
|
|
129
|
+
"ssl-certificates",
|
|
130
|
+
"list",
|
|
131
|
+
"--global",
|
|
132
|
+
`--project=${target.projectId}`,
|
|
133
|
+
"--format=json",
|
|
134
|
+
]);
|
|
135
|
+
if (!r.ok) return { state: "error", error: `list SSL certificates: ${r.stderr.trim()}` };
|
|
136
|
+
const covering = (JSON.parse(r.stdout) as SslCertificateJson[]).filter(
|
|
137
|
+
(c) => c.managed?.domainStatus?.[target.hostname] !== undefined,
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
covering.some(
|
|
141
|
+
(c) =>
|
|
142
|
+
c.managed?.status === "ACTIVE" && c.managed.domainStatus?.[target.hostname] === "ACTIVE",
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return { state: "ready" };
|
|
146
|
+
}
|
|
147
|
+
if (covering.length === 0) {
|
|
148
|
+
return { state: "pending", detail: `no managed certificate lists ${target.hostname} yet` };
|
|
149
|
+
}
|
|
150
|
+
const detail = covering
|
|
151
|
+
.map(
|
|
152
|
+
(c) =>
|
|
153
|
+
`${c.name ?? "certificate"}: ${target.hostname} ${c.managed?.domainStatus?.[target.hostname]}`,
|
|
154
|
+
)
|
|
155
|
+
.join("; ");
|
|
156
|
+
return { state: "pending", detail };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How long the CLI waits for a certificate before handing the operator the
|
|
161
|
+
* status command. Google documents up to 60 min after DNS resolves.
|
|
162
|
+
*/
|
|
163
|
+
export const CERT_WAIT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #656 — progress.json / install.json never hold secret material, and
|
|
5
|
+
* `upgrade` / wizard re-runs scrub what earlier installers stored. Every
|
|
6
|
+
* credential below is a fake fixture.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
statSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join, resolve } from "node:path";
|
|
21
|
+
import { CAELO_VERSION } from "@caelo-cms/shared";
|
|
22
|
+
import {
|
|
23
|
+
assertNoCheckpointSecrets,
|
|
24
|
+
assertNoSecrets,
|
|
25
|
+
checkpointableOutputs,
|
|
26
|
+
findCheckpointSecrets,
|
|
27
|
+
findSecrets,
|
|
28
|
+
redactCredentials,
|
|
29
|
+
rotationHints,
|
|
30
|
+
scrubProgressFile,
|
|
31
|
+
} from "./checkpoint-secrets.js";
|
|
32
|
+
import { databaseUrls } from "./stack-contract.js";
|
|
33
|
+
|
|
34
|
+
const FAKE_PW = "fakePw0rdForTests123";
|
|
35
|
+
const legacyAdminUrl = `postgresql://admin_role:${FAKE_PW}@10.1.2.3:5432/cms_admin?sslmode=require`;
|
|
36
|
+
const legacyPublicUrl = `postgresql://public_role:${FAKE_PW}@10.1.2.3:5432/cms_public?sslmode=require`;
|
|
37
|
+
const setupLink = "https://admin.acme.test/setup?token=fakeSetupToken0123456789";
|
|
38
|
+
|
|
39
|
+
/** What `pulumi up` returns on the current stacks, secret flags included. */
|
|
40
|
+
const stackOutputs = {
|
|
41
|
+
adminDatabaseUrlOut: { value: databaseUrls("10.1.2.3").admin, secret: false },
|
|
42
|
+
publicDatabaseUrlOut: { value: databaseUrls("10.1.2.3").public, secret: false },
|
|
43
|
+
gatewayDatabaseUrlOut: { value: databaseUrls("10.1.2.3").gateway, secret: false },
|
|
44
|
+
bootstrapUrlOut: { value: setupLink, secret: false },
|
|
45
|
+
bootstrapTokenExpiresAtOut: { value: "2026-10-13T00:00:00.000Z", secret: false },
|
|
46
|
+
providerOut: { value: "gcp", secret: false },
|
|
47
|
+
lbIpOut: { value: "203.0.113.7", secret: false },
|
|
48
|
+
adminDomainOut: { value: "admin.acme.test", secret: false },
|
|
49
|
+
adminCloudRunUrlOut: { value: "https://caelo-admin-x.a.run.app", secret: false },
|
|
50
|
+
dnsRecordsRequiredOut: {
|
|
51
|
+
value: [{ hostname: "acme.test", type: "A", value: "203.0.113.7", purpose: "site" }],
|
|
52
|
+
secret: false,
|
|
53
|
+
},
|
|
54
|
+
// A future output Pulumi marks secret, whatever its name or shape.
|
|
55
|
+
someFutureOut: { value: "opaque", secret: true },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A progress.json as installers before #576 / #656 wrote it. */
|
|
59
|
+
const legacyProgress = {
|
|
60
|
+
lastCompletedStep: "pulumi-up-acme",
|
|
61
|
+
updatedAt: "2026-10-01T00:00:00.000Z",
|
|
62
|
+
steps: {
|
|
63
|
+
"project-acme": { existed: true },
|
|
64
|
+
"sa-acme": { created: true, saEmail: "caelo-provisioner@acme.iam.gserviceaccount.com" },
|
|
65
|
+
"sa-key-acme": { path: "/home/op/.caelo-gcp-acme/secrets/sa-key.json" },
|
|
66
|
+
"pulumi-up-acme": {
|
|
67
|
+
outputs: {
|
|
68
|
+
adminDatabaseUrlOut: legacyAdminUrl,
|
|
69
|
+
publicDatabaseUrlOut: legacyPublicUrl,
|
|
70
|
+
bootstrapUrlOut: setupLink,
|
|
71
|
+
providerOut: "gcp",
|
|
72
|
+
lbIpOut: "203.0.113.7",
|
|
73
|
+
adminDomainOut: "admin.acme.test",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("findSecrets", () => {
|
|
80
|
+
it("flags URLs carrying a password and names the database user", () => {
|
|
81
|
+
const found = findSecrets(legacyProgress);
|
|
82
|
+
expect(found).toContainEqual({
|
|
83
|
+
path: "steps.pulumi-up-acme.outputs.adminDatabaseUrlOut",
|
|
84
|
+
kind: "credential-url",
|
|
85
|
+
user: "admin_role",
|
|
86
|
+
});
|
|
87
|
+
expect(found).toContainEqual({
|
|
88
|
+
path: "steps.pulumi-up-acme.outputs.publicDatabaseUrlOut",
|
|
89
|
+
kind: "credential-url",
|
|
90
|
+
user: "public_role",
|
|
91
|
+
});
|
|
92
|
+
expect(found).toContainEqual({
|
|
93
|
+
path: "steps.pulumi-up-acme.outputs.bootstrapUrlOut",
|
|
94
|
+
kind: "token-url",
|
|
95
|
+
});
|
|
96
|
+
// Findings name where, never what.
|
|
97
|
+
expect(JSON.stringify(found)).not.toContain(FAKE_PW);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not flag the password-less database URLs the stacks export today", () => {
|
|
101
|
+
expect(findSecrets(databaseUrls("10.1.2.3"))).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("flags secret-named keys holding a value, not markers or look-alikes", () => {
|
|
105
|
+
expect(findSecrets({ postgresPassword: "x" })).toEqual([
|
|
106
|
+
{ path: "postgresPassword", kind: "secret-key" },
|
|
107
|
+
]);
|
|
108
|
+
expect(findSecrets({ caeloSecretKekOut: "x", anthropic_api_key: "y" })).toHaveLength(2);
|
|
109
|
+
expect(findSecrets({ bootstrapTokenExpiresAtOut: "2026-10-13", hasToken: false })).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("checkpointableOutputs", () => {
|
|
114
|
+
it("drops secret-flagged outputs and the setup link, keeps what resume and DNS need", () => {
|
|
115
|
+
const kept = checkpointableOutputs(stackOutputs);
|
|
116
|
+
expect(Object.keys(kept)).not.toContain("someFutureOut");
|
|
117
|
+
expect(Object.keys(kept)).not.toContain("bootstrapUrlOut");
|
|
118
|
+
expect(kept.dnsRecordsRequiredOut).toEqual(stackOutputs.dnsRecordsRequiredOut.value);
|
|
119
|
+
expect(kept.adminDomainOut).toBe("admin.acme.test");
|
|
120
|
+
expect(kept.bootstrapTokenExpiresAtOut).toBe("2026-10-13T00:00:00.000Z");
|
|
121
|
+
expect(findSecrets(kept)).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("drops a database URL that carries a password even when Pulumi did not mark it secret", () => {
|
|
125
|
+
const kept = checkpointableOutputs({
|
|
126
|
+
adminDatabaseUrlOut: { value: legacyAdminUrl, secret: false },
|
|
127
|
+
});
|
|
128
|
+
expect(kept).toEqual({});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("checkpoint guard (regression #656)", () => {
|
|
133
|
+
it("refuses a payload with a credential URL without echoing the secret", () => {
|
|
134
|
+
let message = "";
|
|
135
|
+
try {
|
|
136
|
+
assertNoSecrets({ outputs: { adminDatabaseUrlOut: legacyAdminUrl } }, "progress.json");
|
|
137
|
+
} catch (e) {
|
|
138
|
+
message = e instanceof Error ? e.message : String(e);
|
|
139
|
+
}
|
|
140
|
+
expect(message).toContain("outputs.adminDatabaseUrlOut: credential-url");
|
|
141
|
+
expect(message).not.toContain(FAKE_PW);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("accepts every payload shape the wizard checkpoints", () => {
|
|
145
|
+
const payloads = [
|
|
146
|
+
{ existed: true },
|
|
147
|
+
{ created: true },
|
|
148
|
+
{ billingAccountId: "000000-000000-000000" },
|
|
149
|
+
{ count: 12 },
|
|
150
|
+
{ existed: true, saEmail: "sa@acme.iam.gserviceaccount.com" },
|
|
151
|
+
{ granted: ["roles/run.admin"] },
|
|
152
|
+
{ path: "/home/op/.caelo-gcp-acme/secrets/sa-key.json" },
|
|
153
|
+
{ outputs: checkpointableOutputs(stackOutputs) },
|
|
154
|
+
{ skipped: "exists" },
|
|
155
|
+
{ bucket: "caelo-production-static-acme" },
|
|
156
|
+
{ adapter: "cloudflare" },
|
|
157
|
+
];
|
|
158
|
+
for (const p of payloads) expect(() => assertNoSecrets(p, "progress.json")).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("every markStepDone call site in the CLI uses a reviewed payload key", () => {
|
|
162
|
+
// A new checkpoint payload must be added here on purpose, after checking
|
|
163
|
+
// it holds no secret. `outputs` must come from pulumiUpGcp, which returns
|
|
164
|
+
// only checkpointableOutputs().
|
|
165
|
+
const reviewed = new Set([
|
|
166
|
+
"existed",
|
|
167
|
+
"created",
|
|
168
|
+
"billingAccountId",
|
|
169
|
+
"count",
|
|
170
|
+
"saEmail",
|
|
171
|
+
"granted",
|
|
172
|
+
"path",
|
|
173
|
+
"outputs",
|
|
174
|
+
"skipped",
|
|
175
|
+
"bucket",
|
|
176
|
+
"adapter",
|
|
177
|
+
]);
|
|
178
|
+
const files = [
|
|
179
|
+
...readdirSync(import.meta.dir).map((f) => join(import.meta.dir, f)),
|
|
180
|
+
...readdirSync(join(import.meta.dir, "wizards")).map((f) =>
|
|
181
|
+
join(import.meta.dir, "wizards", f),
|
|
182
|
+
),
|
|
183
|
+
].filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"));
|
|
184
|
+
const keys: string[] = [];
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const src = readFileSync(file, "utf8");
|
|
187
|
+
for (const m of src.matchAll(/markStepDone\([^,]+,[^,]+,\s*\{([^}]*)\}/g)) {
|
|
188
|
+
for (const part of (m[1] ?? "").split(",")) {
|
|
189
|
+
const key = part.split(":")[0]?.trim();
|
|
190
|
+
if (key) keys.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
195
|
+
expect(keys.filter((k) => !reviewed.has(k))).toEqual([]);
|
|
196
|
+
const gcp = readFileSync(join(import.meta.dir, "wizards", "gcp.ts"), "utf8");
|
|
197
|
+
expect(gcp).toContain("markStepDone(installId, stepName, { outputs: result.outputs })");
|
|
198
|
+
const up = readFileSync(join(import.meta.dir, "wizards", "gcp-pulumi.ts"), "utf8");
|
|
199
|
+
expect(up).toContain("outputs: checkpointableOutputs(result.outputs)");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("checkpoints of projects with secret-sounding ids (codex review)", () => {
|
|
204
|
+
it("treats step names as identifiers, not secret fields", () => {
|
|
205
|
+
const checkpoint = {
|
|
206
|
+
lastCompletedStep: "project-acme-token",
|
|
207
|
+
updatedAt: "t",
|
|
208
|
+
steps: { "project-acme-token": { existed: true }, "billing-acme-secret": { count: 1 } },
|
|
209
|
+
};
|
|
210
|
+
expect(findCheckpointSecrets(checkpoint)).toEqual([]);
|
|
211
|
+
expect(() => assertNoCheckpointSecrets(checkpoint, "progress.json")).not.toThrow();
|
|
212
|
+
// Payloads under such steps are still inspected.
|
|
213
|
+
expect(
|
|
214
|
+
findCheckpointSecrets({ steps: { "project-acme-token": { url: legacyAdminUrl } } }),
|
|
215
|
+
).toEqual([
|
|
216
|
+
{ path: "steps.project-acme-token.url", kind: "credential-url", user: "admin_role" },
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("scrubProgressFile", () => {
|
|
222
|
+
const withFile = (contents: unknown, run: (path: string) => void) => {
|
|
223
|
+
const dir = mkdtempSync(join(tmpdir(), "caelo-scrub-"));
|
|
224
|
+
try {
|
|
225
|
+
const path = join(dir, "progress.json");
|
|
226
|
+
writeFileSync(path, JSON.stringify(contents), { mode: 0o644 });
|
|
227
|
+
run(path);
|
|
228
|
+
} finally {
|
|
229
|
+
rmSync(dir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
it("removes the secrets, keeps every completed step, and leaves the file 0600", () => {
|
|
234
|
+
withFile(legacyProgress, (path) => {
|
|
235
|
+
const { removed } = scrubProgressFile(path);
|
|
236
|
+
expect(removed.map((f) => f.kind).sort()).toEqual([
|
|
237
|
+
"credential-url",
|
|
238
|
+
"credential-url",
|
|
239
|
+
"token-url",
|
|
240
|
+
]);
|
|
241
|
+
const text = readFileSync(path, "utf8");
|
|
242
|
+
expect(text).not.toContain(FAKE_PW);
|
|
243
|
+
expect(text).not.toContain("token=");
|
|
244
|
+
const after = JSON.parse(text);
|
|
245
|
+
// Wizard re-runs resume: every step is still recorded as done.
|
|
246
|
+
expect(Object.keys(after.steps)).toEqual(Object.keys(legacyProgress.steps));
|
|
247
|
+
expect(after.lastCompletedStep).toBe("pulumi-up-acme");
|
|
248
|
+
expect(after.steps["pulumi-up-acme"].outputs).toEqual({
|
|
249
|
+
providerOut: "gcp",
|
|
250
|
+
lbIpOut: "203.0.113.7",
|
|
251
|
+
adminDomainOut: "admin.acme.test",
|
|
252
|
+
});
|
|
253
|
+
expect(statSync(path).mode & 0o777).toBe(0o600);
|
|
254
|
+
expect(readdirSync(join(path, "..")).filter((f) => f.includes(".tmp-"))).toEqual([]);
|
|
255
|
+
// Idempotent.
|
|
256
|
+
expect(scrubProgressFile(path).removed).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("keeps the completed-step markers of a project whose id sounds secret", () => {
|
|
261
|
+
const cp = {
|
|
262
|
+
lastCompletedStep: "pulumi-up-acme-token",
|
|
263
|
+
updatedAt: "t",
|
|
264
|
+
steps: {
|
|
265
|
+
"project-acme-token": { existed: true },
|
|
266
|
+
"pulumi-up-acme-token": { outputs: { adminDatabaseUrlOut: legacyAdminUrl, lbIpOut: "1" } },
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
withFile(cp, (path) => {
|
|
270
|
+
expect(scrubProgressFile(path).removed).toHaveLength(1);
|
|
271
|
+
const after = JSON.parse(readFileSync(path, "utf8"));
|
|
272
|
+
expect(after.steps).toEqual({
|
|
273
|
+
"project-acme-token": { existed: true },
|
|
274
|
+
"pulumi-up-acme-token": { outputs: { lbIpOut: "1" } },
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("leaves a clean file untouched and a missing one alone", () => {
|
|
280
|
+
withFile(
|
|
281
|
+
{ lastCompletedStep: "x", steps: { x: { created: true } }, updatedAt: "t" },
|
|
282
|
+
(path) => {
|
|
283
|
+
const before = readFileSync(path, "utf8");
|
|
284
|
+
expect(scrubProgressFile(path).removed).toEqual([]);
|
|
285
|
+
expect(readFileSync(path, "utf8")).toBe(before);
|
|
286
|
+
expect(scrubProgressFile(`${path}.missing`).removed).toEqual([]);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("rotationHints", () => {
|
|
293
|
+
it("names the rotate-secret command for each leaked role password", () => {
|
|
294
|
+
const hints = rotationHints(
|
|
295
|
+
[
|
|
296
|
+
{ path: "a", kind: "credential-url", user: "admin_role" },
|
|
297
|
+
{ path: "b", kind: "credential-url", user: "public_role" },
|
|
298
|
+
{ path: "c", kind: "credential-url", user: "gateway_role" },
|
|
299
|
+
{ path: "d", kind: "credential-url", user: "admin_role" },
|
|
300
|
+
],
|
|
301
|
+
"gcp-acme",
|
|
302
|
+
);
|
|
303
|
+
expect(hints).toEqual([
|
|
304
|
+
`admin_role's password was stored in progress.json. Rotate it: bunx @caelo-cms/provisioning@${CAELO_VERSION} rotate-secret postgres-password --install gcp-acme`,
|
|
305
|
+
`public_role's password was stored in progress.json. Rotate it: bunx @caelo-cms/provisioning@${CAELO_VERSION} rotate-secret public-role-password --install gcp-acme`,
|
|
306
|
+
`gateway_role's password was stored in progress.json. Rotate it: bunx @caelo-cms/provisioning@${CAELO_VERSION} rotate-secret gateway-role-password --install gcp-acme`,
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("says the setup link needs no rotation", () => {
|
|
311
|
+
const [hint] = rotationHints([{ path: "a", kind: "token-url" }], "gcp-acme");
|
|
312
|
+
expect(hint).toContain("nothing to rotate");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("redactCredentials", () => {
|
|
317
|
+
it("masks URL passwords and leaves the rest readable", () => {
|
|
318
|
+
const out = redactCredentials(`connect failed: ${legacyAdminUrl} (timeout)`);
|
|
319
|
+
expect(out).not.toContain(FAKE_PW);
|
|
320
|
+
expect(out).toContain("postgresql://admin_role:***@10.1.2.3:5432/cms_admin");
|
|
321
|
+
expect(redactCredentials("https://admin.acme.test/x")).toBe("https://admin.acme.test/x");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("install dir end to end (regression #656)", () => {
|
|
326
|
+
it("a wizard's checkpoints leave no secret outside secrets/; legacy files are scrubbed; secrets are refused", () => {
|
|
327
|
+
// installRoot() resolves ~ via os.homedir(), read once at process start —
|
|
328
|
+
// drive install-state in a child process with its own HOME.
|
|
329
|
+
const home = mkdtempSync(join(tmpdir(), "caelo-install-656-"));
|
|
330
|
+
const installId = "gcp-acme";
|
|
331
|
+
const root = join(home, `.caelo-${installId}`);
|
|
332
|
+
try {
|
|
333
|
+
mkdirSync(root, { recursive: true });
|
|
334
|
+
writeFileSync(join(root, "progress.json"), JSON.stringify(legacyProgress), {
|
|
335
|
+
mode: 0o600,
|
|
336
|
+
});
|
|
337
|
+
const mod = resolve(import.meta.dir, "install-state.ts");
|
|
338
|
+
const sec = resolve(import.meta.dir, "checkpoint-secrets.ts");
|
|
339
|
+
const script = `
|
|
340
|
+
const m = await import(${JSON.stringify(mod)});
|
|
341
|
+
const s = await import(${JSON.stringify(sec)});
|
|
342
|
+
const id = ${JSON.stringify(installId)};
|
|
343
|
+
const lines = m.scrubInstallProgress(id);
|
|
344
|
+
m.markStepDone(id, "sa-acme", { created: true, saEmail: "sa@acme.iam.gserviceaccount.com" });
|
|
345
|
+
m.markStepDone(id, "pulumi-up-acme", { outputs: s.checkpointableOutputs(${JSON.stringify(stackOutputs)}) });
|
|
346
|
+
let refused = "";
|
|
347
|
+
try {
|
|
348
|
+
m.markStepDone(id, "bad", { url: ${JSON.stringify(legacyAdminUrl)} });
|
|
349
|
+
} catch (e) { refused = e.message; }
|
|
350
|
+
console.log(JSON.stringify({ lines, refused, done: m.isStepDone(id, "project-acme") }));
|
|
351
|
+
`;
|
|
352
|
+
const child = Bun.spawnSync([process.execPath, "-e", script], {
|
|
353
|
+
env: { ...process.env, HOME: home },
|
|
354
|
+
stderr: "pipe",
|
|
355
|
+
});
|
|
356
|
+
expect(new TextDecoder().decode(child.stderr)).toBe("");
|
|
357
|
+
expect(child.exitCode).toBe(0);
|
|
358
|
+
const result = JSON.parse(new TextDecoder().decode(child.stdout));
|
|
359
|
+
expect(result.done).toBe(true);
|
|
360
|
+
expect(result.refused).toContain("credential-url");
|
|
361
|
+
expect(result.refused).not.toContain(FAKE_PW);
|
|
362
|
+
expect(result.lines.join("\n")).toContain(
|
|
363
|
+
"rotate-secret postgres-password --install gcp-acme",
|
|
364
|
+
);
|
|
365
|
+
expect(result.lines.join("\n")).toContain(
|
|
366
|
+
"rotate-secret public-role-password --install gcp-acme",
|
|
367
|
+
);
|
|
368
|
+
// Nothing under the install dir but secrets/ holds secret material.
|
|
369
|
+
for (const f of readdirSync(root)) {
|
|
370
|
+
if (f === "secrets" || f === "state") continue;
|
|
371
|
+
const text = readFileSync(join(root, f), "utf8");
|
|
372
|
+
expect(findSecrets(JSON.parse(text))).toEqual([]);
|
|
373
|
+
expect(text).not.toContain(FAKE_PW);
|
|
374
|
+
}
|
|
375
|
+
expect(statSync(join(root, "progress.json")).mode & 0o777).toBe(0o600);
|
|
376
|
+
} finally {
|
|
377
|
+
rmSync(home, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
}, 30_000);
|
|
380
|
+
});
|