@caelo-cms/provisioning 0.10.47 → 0.10.49
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/caddy/Caddyfile.production +1 -1
- package/caddy/Caddyfile.staging +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18402 -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/release-manifest.d.ts +2 -41
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +18 -121
- package/dist/release-manifest.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 +26 -2
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +35 -2
- 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 +85 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +122 -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 +133 -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 +437 -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 +300 -0
- package/src/release-manifest.ts +310 -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 +393 -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 +306 -0
- package/src/upgrader-assess.ts +250 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +411 -0
- package/src/upgrader-job.ts +502 -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,243 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where install.json and progress.json are read from and written to (#696).
|
|
5
|
+
*
|
|
6
|
+
* - {@link LocalStateStore}: `~/.caelo-<id>/` — every install before #696,
|
|
7
|
+
* and self-hosted always.
|
|
8
|
+
* - {@link GcsStateStore}: the install's ops bucket
|
|
9
|
+
* (`gs://<project>-caelo-ops/install/`), which is authoritative. A local
|
|
10
|
+
* copy is kept in `~/.caelo-<id>/` so `listInstalls` and the install
|
|
11
|
+
* picker work offline, but every read in a process starts from the
|
|
12
|
+
* bucket and every write goes to the bucket first.
|
|
13
|
+
*
|
|
14
|
+
* Both stores refuse to write secret material (checkpoint-secrets.ts, #656 /
|
|
15
|
+
* #659): install.json and progress.json are read by tooling, backups and
|
|
16
|
+
* agents, and a bucket copy is no place for a database password either. The
|
|
17
|
+
* guard sits inside each store's `write`, so no caller can skip it.
|
|
18
|
+
*
|
|
19
|
+
* The interface is synchronous on purpose: install-state.ts's callers (the
|
|
20
|
+
* wizard steps, `markStepDone`, the lifecycle commands) are synchronous, and
|
|
21
|
+
* the bucket store shells out to `gcloud storage` synchronously.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { hostname } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { assertNoCheckpointSecrets, assertNoSecrets } from "./checkpoint-secrets.js";
|
|
29
|
+
import { type GcloudResult, gcloudEnv } from "./gcloud.js";
|
|
30
|
+
import { OPS_INSTALL_PREFIX } from "./state-backend.js";
|
|
31
|
+
|
|
32
|
+
/** The two documents a store holds. */
|
|
33
|
+
export type StateFile = "install.json" | "progress.json";
|
|
34
|
+
|
|
35
|
+
/** Reads and writes an install's metadata documents. */
|
|
36
|
+
export interface StateStore {
|
|
37
|
+
readonly kind: "local" | "gcs";
|
|
38
|
+
/** The document's text, or null when it does not exist. */
|
|
39
|
+
read(file: StateFile): string | null;
|
|
40
|
+
/** Replace the document. Refuses secret material. */
|
|
41
|
+
write(file: StateFile, value: unknown): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A synchronous gcloud invocation; `input` is fed on stdin. Tests pass a fake. */
|
|
45
|
+
export type SyncGcloud = (args: string[], input?: string) => GcloudResult;
|
|
46
|
+
|
|
47
|
+
/** The real {@link SyncGcloud}: `gcloud` with prompts disabled. */
|
|
48
|
+
export const realSyncGcloud: SyncGcloud = (args, input) => {
|
|
49
|
+
const r = spawnSync("gcloud", args, {
|
|
50
|
+
env: gcloudEnv(),
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
53
|
+
...(input !== undefined ? { input } : { stdio: ["ignore", "pipe", "pipe"] }),
|
|
54
|
+
});
|
|
55
|
+
if (r.error) throw r.error;
|
|
56
|
+
return {
|
|
57
|
+
ok: r.status === 0,
|
|
58
|
+
stdout: r.stdout ?? "",
|
|
59
|
+
stderr: r.stderr ?? "",
|
|
60
|
+
exitCode: r.status ?? 1,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Serialize a document after the checkpoint-secrets guard. install.json is
|
|
66
|
+
* checked for any secret; progress.json with the checkpoint rules (step
|
|
67
|
+
* payloads, outputs).
|
|
68
|
+
*/
|
|
69
|
+
export function serializeGuarded(file: StateFile, value: unknown): string {
|
|
70
|
+
if (file === "install.json") assertNoSecrets(value, file);
|
|
71
|
+
else assertNoCheckpointSecrets(value, file);
|
|
72
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Atomic mode-0600 write (temp file + rename in the same directory). */
|
|
76
|
+
function writeTextAtomic(path: string, text: string): void {
|
|
77
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
78
|
+
try {
|
|
79
|
+
writeFileSync(tmp, text, { mode: 0o600 });
|
|
80
|
+
renameSync(tmp, path);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
rmSync(tmp, { force: true });
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** install.json + progress.json in `~/.caelo-<id>/`. */
|
|
88
|
+
export class LocalStateStore implements StateStore {
|
|
89
|
+
readonly kind = "local" as const;
|
|
90
|
+
constructor(private readonly root: string) {}
|
|
91
|
+
|
|
92
|
+
read(file: StateFile): string | null {
|
|
93
|
+
const path = join(this.root, file);
|
|
94
|
+
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
write(file: StateFile, value: unknown): void {
|
|
98
|
+
writeTextAtomic(join(this.root, file), serializeGuarded(file, value));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** gcloud's wording for an object that is not there. */
|
|
103
|
+
const NOT_FOUND = /matched no objects|No URLs matched|NotFound|404|does not exist/i;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* install.json + progress.json in the ops bucket, mirrored to
|
|
107
|
+
* `~/.caelo-<id>/`. The first read of a document in a process fetches it
|
|
108
|
+
* from the bucket; later reads use the in-memory copy, which every write
|
|
109
|
+
* updates. A bucket error throws — the bucket is the source of truth, so
|
|
110
|
+
* silently using the local mirror could resume from a stale checkpoint.
|
|
111
|
+
*/
|
|
112
|
+
export class GcsStateStore implements StateStore {
|
|
113
|
+
readonly kind = "gcs" as const;
|
|
114
|
+
private readonly cache = new Map<StateFile, string | null>();
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly bucket: string,
|
|
118
|
+
private readonly root: string,
|
|
119
|
+
private readonly run: SyncGcloud = realSyncGcloud,
|
|
120
|
+
) {}
|
|
121
|
+
|
|
122
|
+
/** `gs://<bucket>/install/<file>`. */
|
|
123
|
+
objectUrl(file: StateFile): string {
|
|
124
|
+
return `gs://${this.bucket}/${OPS_INSTALL_PREFIX}/${file}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The document as the bucket holds it right now, without caching it or
|
|
129
|
+
* touching the local mirror — for looking at a bucket this machine may
|
|
130
|
+
* not own (the wizard's "is this project already an install?" check).
|
|
131
|
+
*/
|
|
132
|
+
peek(file: StateFile): string | null {
|
|
133
|
+
const r = this.run(["storage", "cat", this.objectUrl(file)]);
|
|
134
|
+
if (r.ok) return r.stdout;
|
|
135
|
+
if (NOT_FOUND.test(r.stderr)) return null;
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Could not read ${this.objectUrl(file)}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}. Check that you are signed in (gcloud auth login) as an account with access to the install's ops bucket.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
read(file: StateFile): string | null {
|
|
142
|
+
if (this.cache.has(file)) return this.cache.get(file) ?? null;
|
|
143
|
+
const text = this.peek(file);
|
|
144
|
+
this.cache.set(file, text);
|
|
145
|
+
if (text !== null) writeTextAtomic(join(this.root, file), text);
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
write(file: StateFile, value: unknown): void {
|
|
150
|
+
const text = serializeGuarded(file, value);
|
|
151
|
+
const r = this.run(
|
|
152
|
+
["storage", "cp", "-", this.objectUrl(file), "--content-type=application/json"],
|
|
153
|
+
text,
|
|
154
|
+
);
|
|
155
|
+
if (!r.ok) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Could not write ${this.objectUrl(file)}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}. Nothing was changed locally; fix access to the ops bucket and re-run.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
this.cache.set(file, text);
|
|
161
|
+
writeTextAtomic(join(this.root, file), text);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `gs://<bucket>/install/state.lock` — see {@link acquireBucketStateLock}. */
|
|
166
|
+
export function bucketStateLockUrl(bucket: string): string {
|
|
167
|
+
return `gs://${bucket}/${OPS_INSTALL_PREFIX}/state.lock`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Who holds a lock, for the refusal message. */
|
|
171
|
+
function lockBody(purpose: string): string {
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
purpose,
|
|
174
|
+
host: hostname(),
|
|
175
|
+
pid: process.pid,
|
|
176
|
+
startedAt: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The install's state lock in its ops bucket, created only if absent
|
|
182
|
+
* (`--if-generation-match=0`). Held by every writer of a bucket-backed
|
|
183
|
+
* install's state — an installer run's `pulumi up`, `state migrate` — so
|
|
184
|
+
* none writes a backend another is switching away from. A lock left by a
|
|
185
|
+
* crashed run is reported with the command to remove it, never broken
|
|
186
|
+
* automatically.
|
|
187
|
+
*
|
|
188
|
+
* @returns the release function
|
|
189
|
+
*/
|
|
190
|
+
export function acquireBucketStateLock(
|
|
191
|
+
bucket: string,
|
|
192
|
+
purpose: string,
|
|
193
|
+
run: SyncGcloud = realSyncGcloud,
|
|
194
|
+
): () => void {
|
|
195
|
+
const url = bucketStateLockUrl(bucket);
|
|
196
|
+
const r = run(["storage", "cp", "-", url, "--if-generation-match=0"], lockBody(purpose));
|
|
197
|
+
if (!r.ok) {
|
|
198
|
+
if (/PreconditionFailed|412|At least one of the pre-conditions/i.test(r.stderr)) {
|
|
199
|
+
const held = run(["storage", "cat", url]);
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Another run is changing this install's state (${held.ok ? held.stdout.trim() : url}). Re-run once it has finished; if none is running, remove the stale lock: gcloud storage rm ${url}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`Could not take the state lock ${url}: ${r.stderr.trim()}`);
|
|
205
|
+
}
|
|
206
|
+
return () => {
|
|
207
|
+
const rm = run(["storage", "rm", url]);
|
|
208
|
+
if (!rm.ok) throw new Error(`Could not release ${url}; remove it: gcloud storage rm ${url}`);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The same lock for an install whose state is on this machine:
|
|
214
|
+
* `~/.caelo-<id>/state.lock`, created exclusively.
|
|
215
|
+
*
|
|
216
|
+
* @returns the release function
|
|
217
|
+
*/
|
|
218
|
+
export function acquireLocalStateLock(root: string, purpose: string): () => void {
|
|
219
|
+
const path = join(root, "state.lock");
|
|
220
|
+
try {
|
|
221
|
+
writeFileSync(path, lockBody(purpose), { flag: "wx", mode: 0o600 });
|
|
222
|
+
} catch (e) {
|
|
223
|
+
if ((e as NodeJS.ErrnoException).code === "EEXIST") {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Another run is changing this install's state (${readFileSync(path, "utf8").trim()}). Re-run once it has finished; if none is running, remove the stale lock: rm ${path}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw e;
|
|
229
|
+
}
|
|
230
|
+
return () => rmSync(path, { force: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The state lock for whichever backend `bucket` (gcs) or `root` (local) names. */
|
|
234
|
+
export function acquireStateLock(
|
|
235
|
+
backend: { readonly kind: "local" } | { readonly kind: "gcs"; readonly bucket: string },
|
|
236
|
+
root: string,
|
|
237
|
+
purpose: string,
|
|
238
|
+
run: SyncGcloud = realSyncGcloud,
|
|
239
|
+
): () => void {
|
|
240
|
+
return backend.kind === "gcs"
|
|
241
|
+
? acquireBucketStateLock(backend.bucket, purpose, run)
|
|
242
|
+
: acquireLocalStateLock(root, purpose);
|
|
243
|
+
}
|