@caelo-cms/provisioning 0.10.46 → 0.10.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +122 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +291 -83
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The operator's side of a job-mode GCP install (epic #696, Phase 3;
|
|
5
|
+
* install-job.ts has the design): start the install job after the
|
|
6
|
+
* identity-bound bootstrap, attach to one that runs, or show one that
|
|
7
|
+
* finished — and tail it until it is done.
|
|
8
|
+
*
|
|
9
|
+
* Runs in the operator's terminal with the operator's gcloud login; never
|
|
10
|
+
* in the job itself.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CAELO_VERSION } from "@caelo-cms/shared";
|
|
14
|
+
import { cancel, confirm, isCancel, log, note } from "@clack/prompts";
|
|
15
|
+
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
16
|
+
import { formatDnsRecordGroups, groupDnsRecords, type InstallDnsRecord } from "../dns/groups.js";
|
|
17
|
+
import { activeAccount, gcloud } from "../gcloud.js";
|
|
18
|
+
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
19
|
+
import { realSleep } from "../gcloud-retry.js";
|
|
20
|
+
import {
|
|
21
|
+
acquireLaunchLock,
|
|
22
|
+
claimInstallJobStatus,
|
|
23
|
+
decideInstallJob,
|
|
24
|
+
describeExecution,
|
|
25
|
+
INSTALL_CLOUDFLARE_TOKEN_SECRET,
|
|
26
|
+
INSTALL_JOB_NAME,
|
|
27
|
+
INSTALL_SETUP_LINK_SECRET,
|
|
28
|
+
type InstallJobDecision,
|
|
29
|
+
type InstallJobStatus,
|
|
30
|
+
opsImageRef,
|
|
31
|
+
readExecutionLogs,
|
|
32
|
+
readInstallJobStatus,
|
|
33
|
+
readSetupLink,
|
|
34
|
+
startInstallJob,
|
|
35
|
+
storeInstallSecretValue,
|
|
36
|
+
} from "../install-job.js";
|
|
37
|
+
import {
|
|
38
|
+
currentMetadata,
|
|
39
|
+
type InstallMetadata,
|
|
40
|
+
installRoot,
|
|
41
|
+
isStepDone,
|
|
42
|
+
readLocalMetadata,
|
|
43
|
+
readMetadata,
|
|
44
|
+
recordedImageDigests,
|
|
45
|
+
recordImageDigests,
|
|
46
|
+
} from "../install-state.js";
|
|
47
|
+
import { fetchVerifiedReleaseManifest } from "../release-manifest.js";
|
|
48
|
+
import { selfCommand } from "../self-command.js";
|
|
49
|
+
import { attachInstall } from "../state-attach.js";
|
|
50
|
+
import { hasLocalPulumiState, opsBucketName } from "../state-backend.js";
|
|
51
|
+
import { GcsStateStore, realSyncGcloud, type SyncGcloud } from "../state-store.js";
|
|
52
|
+
import { promptProjectId } from "./gcp-prompts.js";
|
|
53
|
+
|
|
54
|
+
/** Side effects of the tail loop, injectable for tests. */
|
|
55
|
+
export interface TailDeps {
|
|
56
|
+
readonly run: GcloudRunner;
|
|
57
|
+
readonly syncRun: SyncGcloud;
|
|
58
|
+
readonly sleep: Sleep;
|
|
59
|
+
readonly print: (line: string) => void;
|
|
60
|
+
/** Poll interval in ms. */
|
|
61
|
+
readonly intervalMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const realTailDeps: TailDeps = {
|
|
65
|
+
run: gcloud,
|
|
66
|
+
syncRun: realSyncGcloud,
|
|
67
|
+
sleep: realSleep,
|
|
68
|
+
print: (line) => process.stdout.write(`${line}\n`),
|
|
69
|
+
intervalMs: 5_000,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Read the bucket's status and the execution's live state, then decide. */
|
|
73
|
+
async function decide(
|
|
74
|
+
run: GcloudRunner,
|
|
75
|
+
syncRun: SyncGcloud,
|
|
76
|
+
target: { projectId: string; region: string },
|
|
77
|
+
): Promise<InstallJobDecision> {
|
|
78
|
+
const status = readInstallJobStatus(opsBucketName(target.projectId), syncRun);
|
|
79
|
+
const live =
|
|
80
|
+
status && status.state === "running"
|
|
81
|
+
? await describeExecution(run, { ...target, execution: status.execution })
|
|
82
|
+
: null;
|
|
83
|
+
const decision = decideInstallJob(status, live);
|
|
84
|
+
// A finished install whose setup link is gone from Secret Manager (deleted
|
|
85
|
+
// or disabled since) runs again: the job's last step stores it anew.
|
|
86
|
+
if (decision.kind === "done" && (await readSetupLink(run, target.projectId)) === null) {
|
|
87
|
+
return {
|
|
88
|
+
kind: "start",
|
|
89
|
+
previous: decision.status,
|
|
90
|
+
why: `the install finished, but its Owner setup link is no longer in Secret Manager (${INSTALL_SETUP_LINK_SECRET}); a new run stores it again`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return decision;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** At most this many polls after the job finished, to catch late log lines. */
|
|
97
|
+
const DRAIN_POLLS = 12;
|
|
98
|
+
/** ...stopping early after this many polls in a row brought nothing new. */
|
|
99
|
+
const DRAIN_QUIET_POLLS = 3;
|
|
100
|
+
|
|
101
|
+
/** How a tail ended. */
|
|
102
|
+
export type TailOutcome =
|
|
103
|
+
| { readonly kind: "succeeded" }
|
|
104
|
+
| { readonly kind: "failed"; readonly status: InstallJobStatus | null; readonly why: string };
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Follow one execution: print its log lines as Cloud Logging receives
|
|
108
|
+
* them and each phase change from the status document, until the job
|
|
109
|
+
* records `succeeded` / `failed` or the execution ends without doing so.
|
|
110
|
+
*/
|
|
111
|
+
export async function tailInstallJob(
|
|
112
|
+
target: { projectId: string; region: string; execution: string },
|
|
113
|
+
deps: TailDeps = realTailDeps,
|
|
114
|
+
): Promise<TailOutcome> {
|
|
115
|
+
const bucket = opsBucketName(target.projectId);
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
let cursor: string | null = null;
|
|
118
|
+
let lastPhase = "";
|
|
119
|
+
const flush = async (): Promise<number> => {
|
|
120
|
+
const logs = await readExecutionLogs(deps.run, target, cursor, seen);
|
|
121
|
+
cursor = logs.cursor;
|
|
122
|
+
for (const line of logs.lines) deps.print(line);
|
|
123
|
+
return logs.lines.length;
|
|
124
|
+
};
|
|
125
|
+
// Cloud Logging ingests asynchronously: the bucket can say "done" before
|
|
126
|
+
// the job's last lines (its summary, the go-live DNS records) are
|
|
127
|
+
// queryable. Keep reading until a few polls in a row bring nothing new,
|
|
128
|
+
// for at most DRAIN_POLLS polls.
|
|
129
|
+
const drain = async (): Promise<void> => {
|
|
130
|
+
let quiet = 0;
|
|
131
|
+
for (let i = 0; i < DRAIN_POLLS && quiet < DRAIN_QUIET_POLLS; i++) {
|
|
132
|
+
await deps.sleep(deps.intervalMs);
|
|
133
|
+
quiet = (await flush()) === 0 ? quiet + 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (let tick = 0; ; tick++) {
|
|
137
|
+
await flush();
|
|
138
|
+
const status = readInstallJobStatus(bucket, deps.syncRun);
|
|
139
|
+
const ours = status?.execution === target.execution ? status : null;
|
|
140
|
+
if (ours && ours.phase !== lastPhase) {
|
|
141
|
+
lastPhase = ours.phase;
|
|
142
|
+
deps.print(bold(cyan(`==> ${ours.phase}`)) + (ours.detail ? `\n ${ours.detail}` : ""));
|
|
143
|
+
}
|
|
144
|
+
if (ours?.state === "succeeded") {
|
|
145
|
+
await drain();
|
|
146
|
+
return { kind: "succeeded" };
|
|
147
|
+
}
|
|
148
|
+
if (ours?.state === "failed") {
|
|
149
|
+
await drain();
|
|
150
|
+
return { kind: "failed", status: ours, why: ours.detail ?? `stopped at "${ours.phase}"` };
|
|
151
|
+
}
|
|
152
|
+
// The job writes its status as it goes; an execution that died (out of
|
|
153
|
+
// memory, task timeout, cancelled) can't. Check it every few polls.
|
|
154
|
+
if (tick % 6 === 5) {
|
|
155
|
+
const live = await describeExecution(deps.run, target);
|
|
156
|
+
if (live !== "running") {
|
|
157
|
+
await drain();
|
|
158
|
+
const final = readInstallJobStatus(bucket, deps.syncRun);
|
|
159
|
+
if (final?.execution === target.execution && final.state === "succeeded") {
|
|
160
|
+
return { kind: "succeeded" };
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
kind: "failed",
|
|
164
|
+
status: final,
|
|
165
|
+
why:
|
|
166
|
+
final?.execution === target.execution && final.state === "failed"
|
|
167
|
+
? (final.detail ?? `stopped at "${final.phase}"`)
|
|
168
|
+
: `execution ${target.execution} ended (${live}) without recording a result`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await deps.sleep(deps.intervalMs);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Where the job's full logs are, for every message that sends the operator there. */
|
|
177
|
+
function logsHint(projectId: string, region: string): string {
|
|
178
|
+
return `gcloud run jobs executions list --job=${INSTALL_JOB_NAME} --region=${region} --project=${projectId}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Print that closing the tab is fine; exit 130 on Ctrl-C with the re-attach hint. */
|
|
182
|
+
function announceDetachable(projectId: string, execution: string): void {
|
|
183
|
+
log.info(
|
|
184
|
+
[
|
|
185
|
+
`The install runs in your project as Cloud Run job ${bold(INSTALL_JOB_NAME)} (execution ${dim(execution)}).`,
|
|
186
|
+
dim(
|
|
187
|
+
"Closing this tab, Ctrl-C or Cloud Shell's idle timeout does not stop it. To follow it again, re-open the",
|
|
188
|
+
),
|
|
189
|
+
dim(`"Open in Cloud Shell" link (or re-run the same command) and pick project ${projectId}.`),
|
|
190
|
+
].join("\n"),
|
|
191
|
+
);
|
|
192
|
+
process.once("SIGINT", () => {
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
`\nStopped following. The install keeps running in ${projectId}; re-run to attach again.\n`,
|
|
195
|
+
);
|
|
196
|
+
process.exit(130);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Tail `execution` and finish the terminal side: the result, or a failure that says what next. */
|
|
201
|
+
async function followToTheEnd(meta: InstallMetadata, execution: string): Promise<void> {
|
|
202
|
+
const projectId = meta.projectId ?? "";
|
|
203
|
+
const region = meta.region ?? "";
|
|
204
|
+
announceDetachable(projectId, execution);
|
|
205
|
+
const outcome = await tailInstallJob({ projectId, region, execution });
|
|
206
|
+
if (outcome.kind === "succeeded") {
|
|
207
|
+
await showResult(meta);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
log.error(red(`The install job failed: ${outcome.why}`));
|
|
211
|
+
log.warn(
|
|
212
|
+
[
|
|
213
|
+
`Fix the cause, then run the installer again — it starts a new run that resumes where this one stopped.`,
|
|
214
|
+
`Full logs: ${logsHint(projectId, region)}`,
|
|
215
|
+
].join("\n"),
|
|
216
|
+
);
|
|
217
|
+
cancel("Install not finished.");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The finished install: the Owner setup link (from Secret Manager) and next steps. */
|
|
222
|
+
async function showResult(meta: InstallMetadata): Promise<void> {
|
|
223
|
+
const projectId = meta.projectId ?? "";
|
|
224
|
+
const link = await readSetupLink(gcloud, projectId);
|
|
225
|
+
if (!link) {
|
|
226
|
+
// decide() starts a new run when the link is missing; getting here means
|
|
227
|
+
// it vanished in between.
|
|
228
|
+
cancel(
|
|
229
|
+
`The Owner setup link disappeared from Secret Manager (${INSTALL_SETUP_LINK_SECRET} in ${projectId}) just now. Run the installer again; it runs the job once more to store it.`,
|
|
230
|
+
);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const adminBase = new URL(link).origin;
|
|
234
|
+
// The records from the stack's outputs, as the pulumi-up step recorded
|
|
235
|
+
// them — not from the log, which a re-run after the install no longer shows.
|
|
236
|
+
// Uncached: this process may hold progress.json from before the job ran.
|
|
237
|
+
const progressText = new GcsStateStore(
|
|
238
|
+
opsBucketName(projectId),
|
|
239
|
+
installRoot(meta.installId),
|
|
240
|
+
realSyncGcloud,
|
|
241
|
+
).peek("progress.json");
|
|
242
|
+
const progress = progressText
|
|
243
|
+
? (JSON.parse(progressText) as { steps?: Record<string, unknown> })
|
|
244
|
+
: {};
|
|
245
|
+
const pulumiUp = progress.steps?.[`pulumi-up-${projectId}`] as
|
|
246
|
+
| { outputs?: Record<string, unknown> }
|
|
247
|
+
| undefined;
|
|
248
|
+
const outputs = pulumiUp?.outputs ?? {};
|
|
249
|
+
const records = Array.isArray(outputs.dnsRecordsRequiredOut)
|
|
250
|
+
? (outputs.dnsRecordsRequiredOut as InstallDnsRecord[])
|
|
251
|
+
: [];
|
|
252
|
+
const dnsLines =
|
|
253
|
+
records.length > 0
|
|
254
|
+
? [
|
|
255
|
+
"",
|
|
256
|
+
bold(`DNS records for ${meta.domain}:`),
|
|
257
|
+
...formatDnsRecordGroups(groupDnsRecords(records, meta.domain), meta.domain).map(
|
|
258
|
+
(l) => ` ${l}`,
|
|
259
|
+
),
|
|
260
|
+
]
|
|
261
|
+
: [];
|
|
262
|
+
note(
|
|
263
|
+
[
|
|
264
|
+
green(`✓ ${meta.domain} is installed.`),
|
|
265
|
+
...dnsLines,
|
|
266
|
+
"",
|
|
267
|
+
bold("Next steps:"),
|
|
268
|
+
` 1. Owner setup — open in your browser (sign in with ${meta.ownerEmail}):`,
|
|
269
|
+
` ${cyan(link)}`,
|
|
270
|
+
` 2. AI provider — visit ${cyan(`${adminBase}/settings/ai`)} and paste your Anthropic API key.`,
|
|
271
|
+
` 3. Set the go-live DNS records above once your new site is published`,
|
|
272
|
+
` (the admin lists them too, at ${cyan(`${adminBase}/settings/domains`)}).`,
|
|
273
|
+
"",
|
|
274
|
+
bold("Later:"),
|
|
275
|
+
` New releases: ${cyan(`${adminBase}/settings/updates`)} shows how to install them.`,
|
|
276
|
+
` Manage the install from any machine: ${dim(selfCommand("attach", "--project", projectId))}`,
|
|
277
|
+
].join("\n"),
|
|
278
|
+
"Done",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* After the bootstrap (gcp.ts, job mode): pin the release, start the job —
|
|
284
|
+
* or attach to / show the one already there — and follow it.
|
|
285
|
+
*/
|
|
286
|
+
export async function launchInstallJob(opts: {
|
|
287
|
+
installId: string;
|
|
288
|
+
projectId: string;
|
|
289
|
+
region: string;
|
|
290
|
+
provisionerEmail: string;
|
|
291
|
+
nonInteractive: boolean;
|
|
292
|
+
}): Promise<void> {
|
|
293
|
+
const { installId, projectId, region } = opts;
|
|
294
|
+
const meta = readMetadata(installId);
|
|
295
|
+
if (!meta) throw new Error(`No install.json for install '${installId}'.`);
|
|
296
|
+
const decision = await decide(gcloud, realSyncGcloud, { projectId, region });
|
|
297
|
+
if (decision.kind === "attach") {
|
|
298
|
+
await followToTheEnd(meta, decision.status.execution);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (decision.kind === "done") {
|
|
302
|
+
await showResult(meta);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (decision.previous) {
|
|
306
|
+
log.warn(yellow(`An earlier install run did not finish: ${decision.why}.`));
|
|
307
|
+
if (!opts.nonInteractive) {
|
|
308
|
+
const again = await confirm({
|
|
309
|
+
message: "Start the install job again? It resumes where the last run stopped.",
|
|
310
|
+
initialValue: true,
|
|
311
|
+
});
|
|
312
|
+
if (isCancel(again) || !again) {
|
|
313
|
+
cancel("Not started.");
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The release this CLI belongs to, verified against Caelo's release
|
|
320
|
+
// workflow identity: its manifest pins the ops image the job runs and,
|
|
321
|
+
// for a new install, the admin + gateway images it deploys.
|
|
322
|
+
const verified = await fetchVerifiedReleaseManifest(CAELO_VERSION);
|
|
323
|
+
if (!verified.ok) {
|
|
324
|
+
cancel(`Could not verify release v${CAELO_VERSION}: ${verified.error.message}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const { digests } = verified.manifest;
|
|
328
|
+
if (!isStepDone(installId, `pulumi-up-${projectId}`) && !recordedImageDigests(meta)) {
|
|
329
|
+
recordImageDigests(installId, { admin: digests.admin, gateway: digests.gateway });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// One launcher at a time decides and starts: two terminals resuming the
|
|
333
|
+
// same install would otherwise both start an execution before either
|
|
334
|
+
// recorded it. Under the lock the decision is taken again.
|
|
335
|
+
const bucket = opsBucketName(projectId);
|
|
336
|
+
let release: () => void;
|
|
337
|
+
try {
|
|
338
|
+
release = acquireLaunchLock(bucket, realSyncGcloud);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
cancel(e instanceof Error ? e.message : String(e));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
let execution: string;
|
|
344
|
+
try {
|
|
345
|
+
const now = await decide(gcloud, realSyncGcloud, { projectId, region });
|
|
346
|
+
if (now.kind !== "start") {
|
|
347
|
+
release();
|
|
348
|
+
if (now.kind === "attach") await followToTheEnd(meta, now.status.execution);
|
|
349
|
+
else await showResult(meta);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Automatic DNS in the job (dns/cloudflare.ts) when this shell has a
|
|
353
|
+
// Cloudflare token: it goes to Secret Manager and is mounted into the
|
|
354
|
+
// job as CLOUDFLARE_API_TOKEN — never into the job definition or args.
|
|
355
|
+
const cloudflareToken = process.env.CLOUDFLARE_API_TOKEN ?? "";
|
|
356
|
+
if (cloudflareToken !== "") {
|
|
357
|
+
const stored = await storeInstallSecretValue(
|
|
358
|
+
gcloud,
|
|
359
|
+
{ projectId, region },
|
|
360
|
+
INSTALL_CLOUDFLARE_TOKEN_SECRET,
|
|
361
|
+
cloudflareToken,
|
|
362
|
+
);
|
|
363
|
+
if (!stored.ok) {
|
|
364
|
+
release();
|
|
365
|
+
cancel(`Could not hand CLOUDFLARE_API_TOKEN to the install job: ${stored.error}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
log.info(
|
|
369
|
+
`DNS: the job sets the records through Cloudflare (token stored in Secret Manager as ${INSTALL_CLOUDFLARE_TOKEN_SECRET}).`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const started = await startInstallJob(gcloud, {
|
|
373
|
+
projectId,
|
|
374
|
+
region,
|
|
375
|
+
serviceAccount: opts.provisionerEmail,
|
|
376
|
+
image: opsImageRef(digests.ops),
|
|
377
|
+
cloudflareToken: cloudflareToken !== "",
|
|
378
|
+
});
|
|
379
|
+
if (!started.ok) {
|
|
380
|
+
release();
|
|
381
|
+
cancel(`Could not start the install job: ${started.error}`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
execution = started.execution;
|
|
385
|
+
const at = new Date().toISOString();
|
|
386
|
+
// The execution is already running and writes its own status; this
|
|
387
|
+
// record only lands if it hasn't yet (claimInstallJobStatus).
|
|
388
|
+
claimInstallJobStatus(
|
|
389
|
+
bucket,
|
|
390
|
+
{
|
|
391
|
+
statusVersion: 1,
|
|
392
|
+
installId,
|
|
393
|
+
execution,
|
|
394
|
+
state: "running",
|
|
395
|
+
phase: "Starting the install job",
|
|
396
|
+
caeloVersion: CAELO_VERSION,
|
|
397
|
+
startedAt: at,
|
|
398
|
+
updatedAt: at,
|
|
399
|
+
},
|
|
400
|
+
realSyncGcloud,
|
|
401
|
+
);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
release();
|
|
404
|
+
throw e;
|
|
405
|
+
}
|
|
406
|
+
release();
|
|
407
|
+
await followToTheEnd(meta, execution);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** What {@link prepareJobModeInstall} found for the project. */
|
|
411
|
+
export type JobModePreparation =
|
|
412
|
+
| { readonly kind: "new"; readonly projectId: string }
|
|
413
|
+
| { readonly kind: "existing"; readonly meta: InstallMetadata }
|
|
414
|
+
| { readonly kind: "handled" };
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Before any other prompt in job mode: the project comes first, because a
|
|
418
|
+
* Cloud Shell session opened by the button starts with an empty home
|
|
419
|
+
* directory — everything known about an install is in its project. An
|
|
420
|
+
* install whose state is there is attached; one whose job is running is
|
|
421
|
+
* followed and one whose job finished is shown, without re-running the
|
|
422
|
+
* bootstrap. Otherwise the wizard continues (new install, or a re-run of an
|
|
423
|
+
* unfinished bootstrap).
|
|
424
|
+
*/
|
|
425
|
+
export async function prepareJobModeInstall(opts: {
|
|
426
|
+
projectId?: string;
|
|
427
|
+
}): Promise<JobModePreparation> {
|
|
428
|
+
const account = await activeAccount();
|
|
429
|
+
if (!account) {
|
|
430
|
+
cancel(
|
|
431
|
+
"No active gcloud account. Run `gcloud auth login` (in Cloud Shell: the first step of the tutorial), then re-run.",
|
|
432
|
+
);
|
|
433
|
+
process.exit(2);
|
|
434
|
+
}
|
|
435
|
+
log.info(`gcloud auth: ${bold(account)}`);
|
|
436
|
+
const projectId = opts.projectId ?? (await promptProjectId("my-caelo-site"));
|
|
437
|
+
const bucket = opsBucketName(projectId);
|
|
438
|
+
let existing: string | null;
|
|
439
|
+
try {
|
|
440
|
+
existing = new GcsStateStore(bucket, "/nonexistent", realSyncGcloud).peek("install.json");
|
|
441
|
+
} catch (e) {
|
|
442
|
+
cancel(e instanceof Error ? e.message : String(e));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
if (existing === null) return { kind: "new", projectId };
|
|
446
|
+
|
|
447
|
+
const attached = attachInstall(projectId, {
|
|
448
|
+
run: realSyncGcloud,
|
|
449
|
+
rootFor: installRoot,
|
|
450
|
+
hasLocalState: hasLocalPulumiState,
|
|
451
|
+
});
|
|
452
|
+
if (!attached.ok) {
|
|
453
|
+
cancel(attached.error);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
const local = readLocalMetadata(attached.installId);
|
|
457
|
+
if (!local) throw new Error(`attach wrote no install.json for ${attached.installId}`);
|
|
458
|
+
const meta = currentMetadata(local);
|
|
459
|
+
log.success(`Found install ${bold(meta.installId)} (${meta.domain}) in ${projectId}.`);
|
|
460
|
+
if (!meta.region) return { kind: "existing", meta };
|
|
461
|
+
const decision = await decide(gcloud, realSyncGcloud, { projectId, region: meta.region });
|
|
462
|
+
if (decision.kind === "attach") {
|
|
463
|
+
await followToTheEnd(meta, decision.status.execution);
|
|
464
|
+
return { kind: "handled" };
|
|
465
|
+
}
|
|
466
|
+
if (decision.kind === "done") {
|
|
467
|
+
await showResult(meta);
|
|
468
|
+
return { kind: "handled" };
|
|
469
|
+
}
|
|
470
|
+
return { kind: "existing", meta };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `status`'s view of the install job: what the bucket records and, for a
|
|
475
|
+
* run that claims to be running, whether its execution still is. Empty
|
|
476
|
+
* when the install never used the job.
|
|
477
|
+
*/
|
|
478
|
+
export async function installJobStatusLines(
|
|
479
|
+
meta: InstallMetadata,
|
|
480
|
+
deps: { run: GcloudRunner; syncRun: SyncGcloud } = { run: gcloud, syncRun: realSyncGcloud },
|
|
481
|
+
): Promise<string[]> {
|
|
482
|
+
if (!meta.projectId || !meta.region) return [];
|
|
483
|
+
const status = readInstallJobStatus(opsBucketName(meta.projectId), deps.syncRun);
|
|
484
|
+
if (!status) return [];
|
|
485
|
+
const decision = decideInstallJob(
|
|
486
|
+
status,
|
|
487
|
+
status.state === "running"
|
|
488
|
+
? await describeExecution(deps.run, {
|
|
489
|
+
projectId: meta.projectId,
|
|
490
|
+
region: meta.region,
|
|
491
|
+
execution: status.execution,
|
|
492
|
+
})
|
|
493
|
+
: null,
|
|
494
|
+
);
|
|
495
|
+
const head = `Install job: ${status.state} — ${status.phase} ${dim(`(v${status.caeloVersion}, execution ${status.execution}, updated ${status.updatedAt})`)}`;
|
|
496
|
+
const lines = [head];
|
|
497
|
+
if (status.detail) lines.push(` ${status.detail}`);
|
|
498
|
+
if (decision.kind === "attach") {
|
|
499
|
+
lines.push(` Follow it: re-run the installer for project ${meta.projectId} (it attaches).`);
|
|
500
|
+
} else if (decision.kind === "start") {
|
|
501
|
+
lines.push(
|
|
502
|
+
` Not finished (${decision.why}). Re-run the installer for project ${meta.projectId}; it resumes where this run stopped.`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return lines;
|
|
506
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { cancel, isCancel, text } from "@clack/prompts";
|
|
4
|
+
|
|
5
|
+
/** Ask for the GCP project id (created if absent), validated like GCP does. */
|
|
6
|
+
export async function promptProjectId(guess: string): Promise<string> {
|
|
7
|
+
const value = await text({
|
|
8
|
+
message: "GCP project id (will be created if absent)",
|
|
9
|
+
placeholder: guess,
|
|
10
|
+
defaultValue: guess,
|
|
11
|
+
validate: (v) => {
|
|
12
|
+
if (!v || v.length < 6 || v.length > 30) {
|
|
13
|
+
return "GCP project id must be 6–30 chars";
|
|
14
|
+
}
|
|
15
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(v)) {
|
|
16
|
+
return "GCP project id: lowercase letters, digits, hyphens; start with letter; no trailing hyphen";
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (isCancel(value)) {
|
|
22
|
+
cancel("Cancelled.");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
return value as string;
|
|
26
|
+
}
|