@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
package/dist/wizards/gcp.js
CHANGED
|
@@ -28,7 +28,7 @@ import { randomBytes } from "node:crypto";
|
|
|
28
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { join } from "node:path";
|
|
31
|
-
import { cancel, confirm, isCancel, log, note, select, spinner
|
|
31
|
+
import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
|
|
32
32
|
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
33
33
|
import { checkAdcImpersonation } from "../adc-impersonation.js";
|
|
34
34
|
import { CERT_WAIT_TIMEOUT_MS, lbCertProbe, waitForCert } from "../cert-wait.js";
|
|
@@ -39,20 +39,27 @@ import { formatDnsRecordGroups, groupDnsRecords } from "../dns/groups.js";
|
|
|
39
39
|
import { pickDnsAdapter } from "../dns/index.js";
|
|
40
40
|
import { ensureGatewayRolePassword, findSqlInstance } from "../gateway-credentials.js";
|
|
41
41
|
import { activeAccount, createProject, createServiceAccount, enableApis, gcloud, gcloudEnv, grantProvisionerRoles, linkBilling, listBillingAccounts, PROVISIONER_ROLE_LIST, projectExists, REQUIRED_API_LIST, serviceAccountExists, } from "../gcloud.js";
|
|
42
|
-
import { GCP_STACK_ENV } from "../gcp-names.js";
|
|
43
|
-
import {
|
|
42
|
+
import { GCP_STACK_ENV, upgraderServiceAccountEmail } from "../gcp-names.js";
|
|
43
|
+
import { INFRA_CONTRACT_VERSION } from "../infra-contract.js";
|
|
44
|
+
import { INSTALL_JOB_NAME, INSTALL_JOB_ROLES, INSTALL_SETUP_LINK_SECRET, storeSetupLink, } from "../install-job.js";
|
|
45
|
+
import { adoptStateBackend, ensureInstallDir, installRoot, installStagingHost, isStepDone, markStepDone, readLocalMetadata, readMetadata, readSecret, recordedImageDigests, recordImageDigests, recordInfraContractVersion, writeMetadata, writeSecret, } from "../install-state.js";
|
|
44
46
|
import { ensureOperatorAccessSync, installIapAllowlist, resolveOperatorAccessTarget, } from "../operator-access.js";
|
|
45
47
|
import { ensureOpsBackend } from "../ops-backend.js";
|
|
46
48
|
import { convergeKeylessProvisioner, PROVISIONER_ACCOUNT_ID, provisionerServiceAccountEmail, } from "../provisioner-identity.js";
|
|
47
49
|
import { decideRegion, detectCliRegion, installableRegions, suggestRegion } from "../regions.js";
|
|
48
50
|
import { selfCommand } from "../self-command.js";
|
|
49
51
|
import { ABSENT_DEFAULT_FIREWALL_RULES } from "../stack-contract.js";
|
|
50
|
-
import { chooseInstallBackend, describeBackend, gcsBackendRecord, hasLocalPulumiState, resolveBackend, } from "../state-backend.js";
|
|
51
|
-
import { GcsStateStore } from "../state-store.js";
|
|
52
|
+
import { chooseInstallBackend, describeBackend, gcsBackendRecord, hasLocalPulumiState, installBackend, parseStateBackend, resolveBackend, } from "../state-backend.js";
|
|
53
|
+
import { acquireStateLock, GcsStateStore } from "../state-store.js";
|
|
54
|
+
import { releaseVersionForDigest, resolveOpsImageRef, verifyCosignAll, } from "../upgrade/release-images.js";
|
|
55
|
+
import { ensureUpgraderJob, resolveUpgraderTarget } from "../upgrader-setup.js";
|
|
52
56
|
import { estimateGcpCost } from "./gcp-cost.js";
|
|
57
|
+
import { promptProjectId } from "./gcp-prompts.js";
|
|
53
58
|
import { GCP_STACK_KNOB_DEFAULTS, pulumiUpGcp, readStackOutput } from "./gcp-pulumi.js";
|
|
54
59
|
export async function runGcpWizard(opts) {
|
|
55
|
-
const { installId
|
|
60
|
+
const { installId } = opts;
|
|
61
|
+
const provider = opts.provider ?? "gcp";
|
|
62
|
+
const runMode = opts.runMode ?? "local";
|
|
56
63
|
const { secretsDir } = ensureInstallDir(installId);
|
|
57
64
|
// === 1. gcloud auth ===
|
|
58
65
|
await stepActiveGcloud();
|
|
@@ -75,7 +82,7 @@ export async function runGcpWizard(opts) {
|
|
|
75
82
|
// === 4. Billing link ===
|
|
76
83
|
await stepBillingLink(installId, projectId, opts.nonInteractive);
|
|
77
84
|
// === 5. Enable APIs ===
|
|
78
|
-
await stepEnableApis(installId, projectId,
|
|
85
|
+
await stepEnableApis(installId, projectId, provider);
|
|
79
86
|
// === 6. Service account + roles + impersonation (no key, #696) ===
|
|
80
87
|
// v0.3.12 — dropped the firebase-hosting-identity step. The
|
|
81
88
|
// Service Usage GenerateServiceIdentity API doesn't support
|
|
@@ -89,19 +96,19 @@ export async function runGcpWizard(opts) {
|
|
|
89
96
|
// didn't.
|
|
90
97
|
const saEmail = provisionerServiceAccountEmail(projectId);
|
|
91
98
|
await stepServiceAccount(installId, projectId, saEmail);
|
|
92
|
-
await stepGrantRoles(installId, projectId, saEmail);
|
|
93
|
-
|
|
99
|
+
await stepGrantRoles(installId, projectId, saEmail, runMode);
|
|
100
|
+
// Pulumi runs here only in local mode; the install job runs AS the
|
|
101
|
+
// provisioner, so this terminal needs no Application Default Credentials.
|
|
102
|
+
await stepKeylessProvisioner(projectId, secretsDir, {
|
|
103
|
+
nonInteractive: opts.nonInteractive,
|
|
104
|
+
checkAdc: runMode === "local",
|
|
105
|
+
});
|
|
94
106
|
// === 8. Cost-estimate pre-flight ===
|
|
95
107
|
// v0.3.3 — provider variant threads through so gcp-firebase
|
|
96
108
|
// drops the LB / Cloud CDN / Cloud Armor lines (saves ~$19/mo
|
|
97
109
|
// vs gcp). Default 'gcp' for backwards compatibility with
|
|
98
110
|
// installs that don't set the field.
|
|
99
|
-
const
|
|
100
|
-
...GCP_STACK_KNOB_DEFAULTS,
|
|
101
|
-
provider: opts.provider ?? "gcp",
|
|
102
|
-
region,
|
|
103
|
-
};
|
|
104
|
-
const estimate = estimateGcpCost(costInputs);
|
|
111
|
+
const estimate = estimateGcpCost({ ...GCP_STACK_KNOB_DEFAULTS, provider, region });
|
|
105
112
|
note([
|
|
106
113
|
bold("Estimated monthly cost (resource floor — actual usage adds AI calls + egress + storage growth)"),
|
|
107
114
|
` ${estimate.regionNote}`,
|
|
@@ -111,6 +118,11 @@ export async function runGcpWizard(opts) {
|
|
|
111
118
|
` ${bold("TOTAL".padEnd(40))} ${bold(`$${estimate.totalUsd}`.padStart(5))}/mo`,
|
|
112
119
|
"",
|
|
113
120
|
` ${dim(`Also deletes the default network's pre-populated firewall rules ${ABSENT_DEFAULT_FIREWALL_RULES.map((r) => r.name).join(" + ")} (Caelo runs no VMs and does not use that network).`)}`,
|
|
121
|
+
...(runMode === "job"
|
|
122
|
+
? [
|
|
123
|
+
` ${dim(`The install itself runs as a one-off Cloud Run job (${INSTALL_JOB_NAME}) in the project: about ${INSTALL_JOB_COST_NOTE} for one install run, nothing while idle.`)}`,
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
114
126
|
].join("\n"), "Pre-flight");
|
|
115
127
|
if (!opts.nonInteractive) {
|
|
116
128
|
const proceed = await confirm({
|
|
@@ -126,7 +138,48 @@ export async function runGcpWizard(opts) {
|
|
|
126
138
|
// A new install: the ops bucket + KMS key in its own project — after the
|
|
127
139
|
// cost confirmation, which lists them (CLAUDE.md §11.C: nothing billable
|
|
128
140
|
// before the y/N). An older install keeps its local state + passphrase.
|
|
129
|
-
const backend = await stepStateBackend(installId, projectId, region, saEmail);
|
|
141
|
+
const backend = await stepStateBackend(installId, projectId, region, saEmail, runMode);
|
|
142
|
+
// === 9. The rest runs where the run mode says (run-mode.ts) ===
|
|
143
|
+
if (runMode === "job") {
|
|
144
|
+
const { launchInstallJob } = await import("./gcp-install-job.js");
|
|
145
|
+
await launchInstallJob({
|
|
146
|
+
installId,
|
|
147
|
+
projectId,
|
|
148
|
+
region,
|
|
149
|
+
provisionerEmail: saEmail,
|
|
150
|
+
nonInteractive: opts.nonInteractive,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await runGcpConverge({
|
|
155
|
+
installId,
|
|
156
|
+
projectId,
|
|
157
|
+
region,
|
|
158
|
+
provider,
|
|
159
|
+
impersonateServiceAccount: saEmail,
|
|
160
|
+
backend,
|
|
161
|
+
nonInteractive: opts.nonInteractive,
|
|
162
|
+
setupLink: "print",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** One-off cost of an install-job run (2 vCPU + 4 GiB for up to ~2 h). */
|
|
166
|
+
const INSTALL_JOB_COST_NOTE = "$0.20-0.40";
|
|
167
|
+
/**
|
|
168
|
+
* The half of an install that needs no operator identity: image digests,
|
|
169
|
+
* `pulumi up`, DNS + certificate waits, migrations, database logins, the
|
|
170
|
+
* operator-access job, the placeholder page and the summary. Runs in the
|
|
171
|
+
* operator's terminal (local mode) or in the install job (install-job.ts).
|
|
172
|
+
* Every step is idempotent or checkpointed in progress.json, so a re-run —
|
|
173
|
+
* or a new job execution — resumes where the last one stopped.
|
|
174
|
+
*/
|
|
175
|
+
export async function runGcpConverge(opts) {
|
|
176
|
+
const { installId, projectId, region, provider, backend } = opts;
|
|
177
|
+
const phase = opts.onPhase ?? (() => { });
|
|
178
|
+
const meta = readMetadata(installId);
|
|
179
|
+
if (!meta)
|
|
180
|
+
throw new Error(`No install.json for install '${installId}'.`);
|
|
181
|
+
const { domain, ownerEmail } = meta;
|
|
182
|
+
const costInputs = { ...GCP_STACK_KNOB_DEFAULTS, provider, region };
|
|
130
183
|
// === 9. Image digests to deploy ===
|
|
131
184
|
// An install that already runs a recorded release (from an earlier
|
|
132
185
|
// wizard run or from `upgrade`) keeps it: re-running the wizard converges
|
|
@@ -137,7 +190,10 @@ export async function runGcpWizard(opts) {
|
|
|
137
190
|
// An install provisioned before the record existed keeps what its
|
|
138
191
|
// services run. A new install resolves the floating `:latest` tag to a
|
|
139
192
|
// fixed sha256 digest: Cloud Run keys revisions by image reference, so a
|
|
140
|
-
// tag would never trigger a new revision.
|
|
193
|
+
// tag would never trigger a new revision. (The install job's launcher
|
|
194
|
+
// records the digests of its signed release first, so a job install never
|
|
195
|
+
// reaches `:latest`.)
|
|
196
|
+
phase("Choosing the release images");
|
|
141
197
|
const deployed = isStepDone(installId, `pulumi-up-${projectId}`);
|
|
142
198
|
if (deployed) {
|
|
143
199
|
// The kept release must be able to use the env the stack deploys.
|
|
@@ -148,7 +204,7 @@ export async function runGcpWizard(opts) {
|
|
|
148
204
|
}
|
|
149
205
|
}
|
|
150
206
|
const choice = await chooseImageDigests({
|
|
151
|
-
recorded:
|
|
207
|
+
recorded: recordedImageDigests(meta),
|
|
152
208
|
deployed,
|
|
153
209
|
readLive: () => readDeployedImageDigests({ projectId, region }),
|
|
154
210
|
resolveLatest: resolveImageDigests,
|
|
@@ -174,17 +230,15 @@ export async function runGcpWizard(opts) {
|
|
|
174
230
|
// Pre-built signed images live at the Caelo-team public AR repo
|
|
175
231
|
// (caelo-website/caelo-cms-images by default). Cloud Run reads them
|
|
176
232
|
// directly with no operator-side IAM binding — anonymous public pull.
|
|
233
|
+
phase("Provisioning the infrastructure (pulumi up, 8-15 min)");
|
|
177
234
|
await stepPulumiUp(installId, {
|
|
178
235
|
projectId,
|
|
179
236
|
domain,
|
|
180
237
|
// #650 — the wizard recorded it (or the install predates the setting).
|
|
181
|
-
stagingDomain: installStagingHost({
|
|
182
|
-
domain,
|
|
183
|
-
stagingDomain: readMetadata(installId)?.stagingDomain,
|
|
184
|
-
}),
|
|
238
|
+
stagingDomain: installStagingHost({ domain, stagingDomain: meta.stagingDomain }),
|
|
185
239
|
ownerEmail,
|
|
186
240
|
region,
|
|
187
|
-
impersonateServiceAccount:
|
|
241
|
+
impersonateServiceAccount: opts.impersonateServiceAccount,
|
|
188
242
|
backend,
|
|
189
243
|
cloudSqlTier: costInputs.cloudSqlTier,
|
|
190
244
|
cloudSqlHa: costInputs.cloudSqlHa,
|
|
@@ -197,7 +251,7 @@ export async function runGcpWizard(opts) {
|
|
|
197
251
|
imageDigests,
|
|
198
252
|
// v0.3.1 — route pulumi up at the right stack folder + use the
|
|
199
253
|
// matching config namespace.
|
|
200
|
-
provider
|
|
254
|
+
provider,
|
|
201
255
|
});
|
|
202
256
|
recordImageDigests(installId, imageDigests);
|
|
203
257
|
// === 10.1. Remove the default network's pre-populated firewall rules ===
|
|
@@ -220,8 +274,9 @@ export async function runGcpWizard(opts) {
|
|
|
220
274
|
// The DNS records come first: the certificate only validates once the
|
|
221
275
|
// admin host resolves to the load balancer. Only the "set now" records
|
|
222
276
|
// are applied here — the main domain moves at go-live (dns/groups.ts).
|
|
223
|
-
if (
|
|
224
|
-
await stepGcpDnsRecords(installId);
|
|
277
|
+
if (provider === "gcp") {
|
|
278
|
+
await stepGcpDnsRecords(installId, { nonInteractive: opts.nonInteractive, onPhase: phase });
|
|
279
|
+
phase(`Waiting for the TLS certificate of admin.${domain} (15-60 min)`);
|
|
225
280
|
await stepWaitForCertActive(projectId, `admin.${domain}`);
|
|
226
281
|
}
|
|
227
282
|
// === 11. Apply DB migrations against Cloud SQL via a one-shot Job ===
|
|
@@ -233,6 +288,7 @@ export async function runGcpWizard(opts) {
|
|
|
233
288
|
// P21 ship 3 — shared with `cms-provision upgrade`. Aborts the
|
|
234
289
|
// wizard with a clear error if migrations fail (vs. silently
|
|
235
290
|
// continuing with a half-migrated DB).
|
|
291
|
+
phase("Applying database migrations");
|
|
236
292
|
{
|
|
237
293
|
const { runMigrationsViaCloudRunJob } = await import("../migration-runner.js");
|
|
238
294
|
const r = await runMigrationsViaCloudRunJob({ projectId, region });
|
|
@@ -246,9 +302,27 @@ export async function runGcpWizard(opts) {
|
|
|
246
302
|
// gateway-role-password secret's value here. The gateway (deployed by
|
|
247
303
|
// Pulumi before migrations) retries its database on every request, so it
|
|
248
304
|
// starts serving /api/* as soon as this lands — no restart.
|
|
305
|
+
phase("Finishing: database logins, operator access, welcome page");
|
|
249
306
|
await stepGatewayRolePassword(projectId);
|
|
307
|
+
// === 11.4. The upgrader job: Settings → Updates → Install update (#696) ===
|
|
308
|
+
// Before operator access, which keeps the upgrader's caelo-mcp grant.
|
|
309
|
+
const releaseVersion = await releaseVersionForDigest(gcloud, "admin", imageDigests.admin);
|
|
310
|
+
const upgraderReady = await stepUpgraderJob(projectId, region, releaseVersion);
|
|
311
|
+
const keepSigner = upgraderReady || (await upgraderExists(projectId));
|
|
250
312
|
// === 11.5. Operator access: IAP follows the Caelo user list ===
|
|
251
|
-
await stepOperatorAccessSync(projectId, region,
|
|
313
|
+
await stepOperatorAccessSync(projectId, region, provider, ownerEmail, keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : []);
|
|
314
|
+
// A fresh install ran every converge step above, so it is at this CLI's
|
|
315
|
+
// infrastructure contract — recorded only when the upgrader is in place
|
|
316
|
+
// (the last step a contract bump added). With the release version, the
|
|
317
|
+
// admin can install the next update itself.
|
|
318
|
+
if (upgraderReady) {
|
|
319
|
+
const version = releaseVersion;
|
|
320
|
+
recordImageDigests(installId, imageDigests, version);
|
|
321
|
+
recordInfraContractVersion(installId, INFRA_CONTRACT_VERSION);
|
|
322
|
+
if (!version) {
|
|
323
|
+
log.warn(yellow("Could not tell which Caelo release the admin runs, so in-product updates stay off until the first `upgrade`."));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
252
326
|
// === 12. Upload the fresh-install placeholder to the static bucket ===
|
|
253
327
|
// Without this, https://<domain>/ returns the raw GCS NoSuchKey XML
|
|
254
328
|
// until the operator publishes their first deploy. Idempotent: skips
|
|
@@ -259,16 +333,13 @@ export async function runGcpWizard(opts) {
|
|
|
259
333
|
// Hosting, not a GCS bucket; the static-publisher's first deploy
|
|
260
334
|
// creates the initial Firebase Hosting version. Skip the GCS
|
|
261
335
|
// placeholder upload on that provider.
|
|
262
|
-
if (
|
|
336
|
+
if (provider === "gcp") {
|
|
263
337
|
await stepUploadStaticPlaceholder(installId, projectId, domain);
|
|
264
338
|
}
|
|
265
339
|
// === 12. DNS records + bootstrap URL ===
|
|
266
340
|
// IAP is enabled directly on the LB BackendService (Pulumi-managed);
|
|
267
341
|
// no post-up gcloud step needed.
|
|
268
|
-
await stepFinalize(installId);
|
|
269
|
-
// Reference unused params to silence the linter.
|
|
270
|
-
void domain;
|
|
271
|
-
void ownerEmail;
|
|
342
|
+
await stepFinalize(installId, opts.setupLink);
|
|
272
343
|
}
|
|
273
344
|
/**
|
|
274
345
|
* Resolve the floating `:latest` tag on each service's public AR image
|
|
@@ -343,25 +414,7 @@ async function stepProjectId(opts) {
|
|
|
343
414
|
return opts.projectId;
|
|
344
415
|
}
|
|
345
416
|
const guess = opts.domain.split(".")[0]?.replace(/[^a-z0-9-]/g, "-") ?? "caelo";
|
|
346
|
-
|
|
347
|
-
message: "GCP project id (will be created if absent)",
|
|
348
|
-
placeholder: guess,
|
|
349
|
-
defaultValue: guess,
|
|
350
|
-
validate: (v) => {
|
|
351
|
-
if (!v || v.length < 6 || v.length > 30) {
|
|
352
|
-
return "GCP project id must be 6–30 chars";
|
|
353
|
-
}
|
|
354
|
-
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(v)) {
|
|
355
|
-
return "GCP project id: lowercase letters, digits, hyphens; start with letter; no trailing hyphen";
|
|
356
|
-
}
|
|
357
|
-
return undefined;
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
if (isCancel(value)) {
|
|
361
|
-
cancel("Cancelled.");
|
|
362
|
-
process.exit(0);
|
|
363
|
-
}
|
|
364
|
-
return value;
|
|
417
|
+
return promptProjectId(guess);
|
|
365
418
|
}
|
|
366
419
|
/**
|
|
367
420
|
* Settle the install region (#607). A recorded region — or, for an install
|
|
@@ -566,7 +619,35 @@ async function stepServiceAccount(installId, projectId, saEmail) {
|
|
|
566
619
|
s.stop(green(`Provisioner SA ${bold(saEmail)} created`));
|
|
567
620
|
markStepDone(installId, stepName, { created: true, saEmail });
|
|
568
621
|
}
|
|
569
|
-
async function stepGrantRoles(installId, projectId, saEmail) {
|
|
622
|
+
async function stepGrantRoles(installId, projectId, saEmail, runMode) {
|
|
623
|
+
await stepGrantBaseRoles(installId, projectId, saEmail);
|
|
624
|
+
if (runMode === "job")
|
|
625
|
+
await stepGrantInstallJobRoles(installId, projectId, saEmail);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* #696 — the roles the provisioner needs when it runs the install job
|
|
629
|
+
* itself (install-job.ts INSTALL_JOB_ROLES). Only for installs that use it.
|
|
630
|
+
*/
|
|
631
|
+
async function stepGrantInstallJobRoles(installId, projectId, saEmail) {
|
|
632
|
+
const stepName = `grant-install-job-roles-${projectId}`;
|
|
633
|
+
if (isStepDone(installId, stepName)) {
|
|
634
|
+
log.success(`Install-job roles granted ${dim("(checkpointed)")}`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const s = spinner();
|
|
638
|
+
s.start(`Granting the provisioner ${INSTALL_JOB_ROLES.length} more roles to run the install job...`);
|
|
639
|
+
const { granted, failed } = await grantProvisionerRoles(projectId, saEmail, {
|
|
640
|
+
roles: INSTALL_JOB_ROLES,
|
|
641
|
+
});
|
|
642
|
+
if (failed.length > 0) {
|
|
643
|
+
s.stop(red(`Failed: ${failed.map((f) => `${f.role}: ${f.error}`).join("; ")}`));
|
|
644
|
+
cancel("Aborted — re-run the installer (idempotent).");
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
s.stop(green(`${granted} install-job roles granted`));
|
|
648
|
+
markStepDone(installId, stepName, { granted });
|
|
649
|
+
}
|
|
650
|
+
async function stepGrantBaseRoles(installId, projectId, saEmail) {
|
|
570
651
|
const stepName = `grant-roles-${projectId}`;
|
|
571
652
|
if (isStepDone(installId, stepName)) {
|
|
572
653
|
log.success(`IAM roles granted ${dim(`(${PROVISIONER_ROLE_LIST.length} roles, checkpointed)`)}`);
|
|
@@ -608,13 +689,69 @@ async function stepGatewayRolePassword(projectId) {
|
|
|
608
689
|
}
|
|
609
690
|
s.stop(green("gateway_role ready"));
|
|
610
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Set up the upgrader job (upgrader-setup.ts, #696): what the admin's
|
|
694
|
+
* "Install update" starts. After migrations, because the job's database
|
|
695
|
+
* user gets the role migration 0267 creates. A failure does not stop the
|
|
696
|
+
* install — the site works; updates then go through the CLI until a later
|
|
697
|
+
* `upgrade` sets the job up — so it only warns. Idempotent.
|
|
698
|
+
*
|
|
699
|
+
* @returns whether the job is ready
|
|
700
|
+
*/
|
|
701
|
+
async function stepUpgraderJob(projectId, region, releaseVersion) {
|
|
702
|
+
// The upgrader of the release the install runs (a re-run keeps an older
|
|
703
|
+
// release — a newer upgrader would meet a schema it does not know yet).
|
|
704
|
+
const opsImageRef = releaseVersion ? await resolveOpsImageRef(releaseVersion) : null;
|
|
705
|
+
const digest = opsImageRef ? /@(sha256:[0-9a-f]{64})$/.exec(opsImageRef)?.[1] : undefined;
|
|
706
|
+
// The job runs with rights over the whole app: only a verified image.
|
|
707
|
+
if (digest && !(await verifyCosignAll([{ slug: "ops", digest }]))) {
|
|
708
|
+
log.warn(`The upgrader is not set up: its image's signature did not verify. The install works; ${bold(selfCommand("upgrade"))} sets it up later.`);
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
const s = spinner();
|
|
712
|
+
s.start("Setting up the upgrader job (install updates from the admin)...");
|
|
713
|
+
const resolved = opsImageRef
|
|
714
|
+
? await resolveUpgraderTarget({ projectId, region, env: GCP_STACK_ENV, opsImageRef })
|
|
715
|
+
: {
|
|
716
|
+
ok: false,
|
|
717
|
+
error: releaseVersion
|
|
718
|
+
? `release ${releaseVersion} has no caelo-ops image in the registry`
|
|
719
|
+
: "the release this install runs could not be told from its images",
|
|
720
|
+
};
|
|
721
|
+
const r = resolved.ok
|
|
722
|
+
? await ensureUpgraderJob(resolved.target)
|
|
723
|
+
: { ok: false, done: [], error: resolved.error };
|
|
724
|
+
if (!r.ok) {
|
|
725
|
+
s.stop(yellow(`Upgrader job not set up: ${r.error}`));
|
|
726
|
+
log.warn(`The install works; Settings → Updates shows the upgrade command instead of an Install button until ${bold(selfCommand("upgrade"))} sets the job up.`);
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
s.stop(green(`Upgrader job ready (${r.done.length} steps)`));
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Whether an upgrader set up by an earlier run exists, so a failed setup
|
|
734
|
+
* this time still keeps its caelo-mcp signing grant (the operator-access
|
|
735
|
+
* job would otherwise remove it, breaking its health check).
|
|
736
|
+
*/
|
|
737
|
+
async function upgraderExists(projectId) {
|
|
738
|
+
const r = await gcloud([
|
|
739
|
+
"iam",
|
|
740
|
+
"service-accounts",
|
|
741
|
+
"describe",
|
|
742
|
+
upgraderServiceAccountEmail(projectId, GCP_STACK_ENV),
|
|
743
|
+
`--project=${projectId}`,
|
|
744
|
+
"--format=value(email)",
|
|
745
|
+
]);
|
|
746
|
+
return r.ok;
|
|
747
|
+
}
|
|
611
748
|
/**
|
|
612
749
|
* Set up the operator-access sync job (operator-access.ts): the only
|
|
613
750
|
* principal that may change who passes IAP, run as the operator's gcloud
|
|
614
751
|
* identity. After migrations, because the job's database user gets the role
|
|
615
752
|
* migration 0239 creates. Idempotent, so it runs on every wizard pass.
|
|
616
753
|
*/
|
|
617
|
-
async function stepOperatorAccessSync(projectId, region, provider, ownerEmail) {
|
|
754
|
+
async function stepOperatorAccessSync(projectId, region, provider, ownerEmail, mcpSigners) {
|
|
618
755
|
const s = spinner();
|
|
619
756
|
s.start("Setting up the operator-access sync job (Google IAP follows the user list)...");
|
|
620
757
|
const resolved = await resolveOperatorAccessTarget({
|
|
@@ -625,7 +762,7 @@ async function stepOperatorAccessSync(projectId, region, provider, ownerEmail) {
|
|
|
625
762
|
ownerEmail,
|
|
626
763
|
});
|
|
627
764
|
const r = resolved.ok
|
|
628
|
-
? await ensureOperatorAccessSync(resolved.target)
|
|
765
|
+
? await ensureOperatorAccessSync({ ...resolved.target, mcpSigners })
|
|
629
766
|
: { ok: false, done: [], error: resolved.error };
|
|
630
767
|
if (!r.ok) {
|
|
631
768
|
s.stop(red(`Failed: ${r.error}`));
|
|
@@ -667,7 +804,7 @@ async function stepRemoveDefaultFirewallRules(projectId) {
|
|
|
667
804
|
* Application Default Credentials impersonating the SA — is verified before
|
|
668
805
|
* anything billable runs. Idempotent; runs on every wizard pass.
|
|
669
806
|
*/
|
|
670
|
-
async function stepKeylessProvisioner(projectId, secretsDir,
|
|
807
|
+
async function stepKeylessProvisioner(projectId, secretsDir, opts) {
|
|
671
808
|
const sa = provisionerServiceAccountEmail(projectId);
|
|
672
809
|
const account = await activeAccount();
|
|
673
810
|
if (!account) {
|
|
@@ -702,6 +839,8 @@ async function stepKeylessProvisioner(projectId, secretsDir, nonInteractive) {
|
|
|
702
839
|
for (const line of converged.lines)
|
|
703
840
|
log.info(` ${line}`);
|
|
704
841
|
}
|
|
842
|
+
if (!opts.checkAdc)
|
|
843
|
+
return;
|
|
705
844
|
const check = async () => checkAdcImpersonation({
|
|
706
845
|
projectId,
|
|
707
846
|
operatorAccount: account,
|
|
@@ -710,7 +849,7 @@ async function stepKeylessProvisioner(projectId, secretsDir, nonInteractive) {
|
|
|
710
849
|
const sAdc = spinner();
|
|
711
850
|
sAdc.start("Checking that your Application Default Credentials can impersonate it...");
|
|
712
851
|
let adc = await check();
|
|
713
|
-
if (!adc.ok && adc.reason === "no-adc" && !nonInteractive) {
|
|
852
|
+
if (!adc.ok && adc.reason === "no-adc" && !opts.nonInteractive) {
|
|
714
853
|
sAdc.stop(yellow("No Application Default Credentials yet"));
|
|
715
854
|
const login = await confirm({
|
|
716
855
|
message: `Run ${bold("gcloud auth application-default login")} now (opens your browser; sign in as ${account})?`,
|
|
@@ -745,15 +884,21 @@ async function stepKeylessProvisioner(projectId, secretsDir, nonInteractive) {
|
|
|
745
884
|
* that install's record; this install's own copy from an interrupted run is
|
|
746
885
|
* taken over.
|
|
747
886
|
*/
|
|
748
|
-
async function stepStateBackend(installId, projectId, region, provisionerEmail) {
|
|
887
|
+
async function stepStateBackend(installId, projectId, region, provisionerEmail, runMode) {
|
|
749
888
|
const root = installRoot(installId);
|
|
750
889
|
const meta = readMetadata(installId);
|
|
751
890
|
if (!meta)
|
|
752
891
|
throw new Error(`No install.json for install '${installId}'.`);
|
|
753
892
|
const kind = chooseInstallBackend(meta, hasLocalPulumiState(root), isStepDone(installId, `pulumi-up-${projectId}`));
|
|
893
|
+
if (kind === "local" && runMode === "job") {
|
|
894
|
+
// The job reads the state from the project; a passphrase-encrypted
|
|
895
|
+
// state on this machine is out of its reach.
|
|
896
|
+
cancel(`This install's Pulumi state is on this machine (${describeBackend(meta, root)}), so the install job can't run it. Move it into project ${projectId} first — ${selfCommand("state", "migrate", "--install", installId)} — or re-run with --local.`);
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
754
899
|
if (kind === "local") {
|
|
755
900
|
stepPulumiPassphrase(installId);
|
|
756
|
-
log.info(`Pulumi state: ${dim(describeBackend(meta, root))} (this install predates state in project ${projectId}
|
|
901
|
+
log.info(`Pulumi state: ${dim(describeBackend(meta, root))} (this install predates state in project ${projectId}). Move it there, so any machine you sign in on can manage the install: ${bold(selfCommand("state", "migrate", "--install", installId))}`);
|
|
757
902
|
return resolveBackend(meta, {
|
|
758
903
|
installRoot: root,
|
|
759
904
|
readPassphrase: () => readSecret(installId, "pulumi-passphrase"),
|
|
@@ -791,7 +936,7 @@ async function stepStateBackend(installId, projectId, region, provisionerEmail)
|
|
|
791
936
|
// run, interrupted between writing the bucket and the local copy:
|
|
792
937
|
// finishing the move is safe. Anything else is another install.
|
|
793
938
|
if (other && !(other.installId === meta.installId && other.createdAt === meta.createdAt)) {
|
|
794
|
-
cancel(`Project ${projectId} already has a Caelo install (${other.domain ?? "unknown domain"}) whose state lives in gs://${record.bucket}. This run would overwrite that install's record
|
|
939
|
+
cancel(`Project ${projectId} already has a Caelo install (${other.domain ?? "unknown domain"}) whose state lives in gs://${record.bucket}. This run would overwrite that install's record. To manage it from this machine run: ${selfCommand("attach", "--project", projectId)}`);
|
|
795
940
|
process.exit(1);
|
|
796
941
|
}
|
|
797
942
|
adoptStateBackend(installId, record);
|
|
@@ -815,33 +960,77 @@ function stepPulumiPassphrase(installId) {
|
|
|
815
960
|
}
|
|
816
961
|
async function stepPulumiUp(installId, opts) {
|
|
817
962
|
const stepName = `pulumi-up-${opts.projectId}`;
|
|
818
|
-
|
|
963
|
+
const deployed = isStepDone(installId, stepName);
|
|
964
|
+
if (deployed) {
|
|
819
965
|
log.success(`Pulumi up ${dim("(checkpointed — re-running for drift refresh)")}`);
|
|
820
966
|
}
|
|
821
967
|
const { secretsDir } = ensureInstallDir(installId);
|
|
822
968
|
const root = installRoot(installId);
|
|
969
|
+
// #696 — hold the install's state lock through `pulumi up` (`state
|
|
970
|
+
// migrate` takes the same one), then re-check, uncached, that the state
|
|
971
|
+
// still lives where this run resolved it: the bucket's install.json for a
|
|
972
|
+
// bucket-backed install (another machine may have moved it back), the
|
|
973
|
+
// file on disk for a local one.
|
|
974
|
+
const opsBucket = gcsBackendRecord(opts.projectId, opts.region).bucket;
|
|
975
|
+
let releaseLock;
|
|
976
|
+
try {
|
|
977
|
+
releaseLock = acquireStateLock(opts.backend.kind === "gcs" ? { kind: "gcs", bucket: opsBucket } : { kind: "local" }, root, "installer pulumi up");
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
cancel(e instanceof Error ? e.message : String(e));
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
let moved;
|
|
984
|
+
try {
|
|
985
|
+
const nowText = opts.backend.kind === "gcs"
|
|
986
|
+
? new GcsStateStore(opsBucket, root).peek("install.json")
|
|
987
|
+
: JSON.stringify(readLocalMetadata(installId));
|
|
988
|
+
const now = nowText ? JSON.parse(nowText) : null;
|
|
989
|
+
moved =
|
|
990
|
+
!now ||
|
|
991
|
+
installBackend({ stateBackend: parseStateBackend(now.stateBackend) }).kind !==
|
|
992
|
+
opts.backend.kind;
|
|
993
|
+
}
|
|
994
|
+
catch (e) {
|
|
995
|
+
releaseLock();
|
|
996
|
+
throw e;
|
|
997
|
+
}
|
|
998
|
+
if (moved) {
|
|
999
|
+
releaseLock();
|
|
1000
|
+
cancel("The install's Pulumi state has moved (state migrate) since this run started. Re-run the installer.");
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
823
1003
|
log.info(`Pulumi up — wall-clock 8–15 min (Cloud SQL is the long pole). Streaming progress...`);
|
|
824
1004
|
let resourceCount = 0;
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1005
|
+
let result;
|
|
1006
|
+
try {
|
|
1007
|
+
result = await pulumiUpGcp({
|
|
1008
|
+
installId,
|
|
1009
|
+
installRoot: root,
|
|
1010
|
+
secretsDir,
|
|
1011
|
+
...opts,
|
|
1012
|
+
// A deployed install's stack must exist: a missing one means the
|
|
1013
|
+
// state is elsewhere, and a fresh stack would try to recreate it all.
|
|
1014
|
+
stackMustExist: deployed,
|
|
1015
|
+
provider: opts.provider ?? "gcp",
|
|
1016
|
+
}, (kind, message) => {
|
|
1017
|
+
if (kind === "resource") {
|
|
1018
|
+
resourceCount++;
|
|
1019
|
+
if (resourceCount % 5 === 0) {
|
|
1020
|
+
process.stdout.write(`\r${dim(` ${resourceCount} resources updated`)}`);
|
|
1021
|
+
}
|
|
836
1022
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
1023
|
+
else if (kind === "error") {
|
|
1024
|
+
process.stdout.write("\n");
|
|
1025
|
+
log.error(red(redactCredentials(message)));
|
|
1026
|
+
}
|
|
1027
|
+
// logs: silent — too noisy for a live progress UI; pulumi
|
|
1028
|
+
// diagnostics will print on failure via the SDK's onOutput.
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
releaseLock();
|
|
1033
|
+
}
|
|
845
1034
|
process.stdout.write("\n");
|
|
846
1035
|
log.success(green(`Pulumi up complete — ${result.resourceCount.created} created, ${result.resourceCount.updated} updated, ${result.resourceCount.deleted} deleted`));
|
|
847
1036
|
markStepDone(installId, stepName, { outputs: result.outputs });
|
|
@@ -1228,13 +1417,14 @@ const ADAPTER_RECORD_TYPES = new Set(["A", "AAAA", "CNAME", "TXT"]);
|
|
|
1228
1417
|
* live site are never applied here — the summary lists them for go-live.
|
|
1229
1418
|
* Idempotent: a finished step is skipped on re-runs.
|
|
1230
1419
|
*/
|
|
1231
|
-
async function stepGcpDnsRecords(installId) {
|
|
1420
|
+
async function stepGcpDnsRecords(installId, opts) {
|
|
1232
1421
|
const meta = readMetadata(installId);
|
|
1233
1422
|
if (!meta?.projectId)
|
|
1234
1423
|
return;
|
|
1235
1424
|
const stepName = `dns-${meta.projectId}`;
|
|
1236
1425
|
if (isStepDone(installId, stepName))
|
|
1237
1426
|
return;
|
|
1427
|
+
opts.onPhase("Setting up DNS");
|
|
1238
1428
|
const records = stackDnsRecords(await recordedStackOutputs(installId, meta.projectId));
|
|
1239
1429
|
if (records.length === 0) {
|
|
1240
1430
|
cancel("The stack reported no DNS records (dnsRecordsRequiredOut). Re-run the installer to refresh its outputs.");
|
|
@@ -1245,8 +1435,12 @@ async function stepGcpDnsRecords(installId) {
|
|
|
1245
1435
|
log.warn(yellow(`Not touching ${meta.domain} itself: its records switch a live website to Caelo and are listed at the end for go-live.`));
|
|
1246
1436
|
}
|
|
1247
1437
|
const toApply = setNow.filter((r) => ADAPTER_RECORD_TYPES.has(r.type));
|
|
1248
|
-
const adapter = await pickDnsAdapter({
|
|
1438
|
+
const adapter = await pickDnsAdapter({
|
|
1439
|
+
domain: meta.domain,
|
|
1440
|
+
nonInteractive: opts.nonInteractive,
|
|
1441
|
+
});
|
|
1249
1442
|
log.info(`DNS adapter: ${bold(adapter.name)}`);
|
|
1443
|
+
opts.onPhase("Waiting for DNS", `Set at your registrar: ${toApply.map((r) => `${r.type} ${r.hostname} → ${r.value}`).join("; ")}`);
|
|
1250
1444
|
try {
|
|
1251
1445
|
await adapter.applyRecords(toApply.map((r) => ({ hostname: r.hostname, type: r.type, value: r.value })));
|
|
1252
1446
|
markStepDone(installId, stepName, { adapter: adapter.name });
|
|
@@ -1258,7 +1452,7 @@ async function stepGcpDnsRecords(installId) {
|
|
|
1258
1452
|
process.exit(1);
|
|
1259
1453
|
}
|
|
1260
1454
|
}
|
|
1261
|
-
async function stepFinalize(installId) {
|
|
1455
|
+
async function stepFinalize(installId, setupLink) {
|
|
1262
1456
|
const meta = readMetadata(installId);
|
|
1263
1457
|
if (!meta?.projectId)
|
|
1264
1458
|
return;
|
|
@@ -1280,6 +1474,20 @@ async function stepFinalize(installId) {
|
|
|
1280
1474
|
const bootstrapUrl = provider === "gcp-firebase" && adminCloudRunUrl.length > 0
|
|
1281
1475
|
? `${adminCloudRunUrl}/setup?token=${bootstrapToken}`
|
|
1282
1476
|
: stackBootstrapUrl;
|
|
1477
|
+
// #696 — the install job's output lands in Cloud Logging: the link goes to
|
|
1478
|
+
// Secret Manager, where the operator's terminal reads it (install-job.ts).
|
|
1479
|
+
if (setupLink === "secret") {
|
|
1480
|
+
if (!meta.region)
|
|
1481
|
+
throw new Error(`install.json of ${installId} records no region.`);
|
|
1482
|
+
const stored = await storeSetupLink(gcloud, { projectId: meta.projectId, region: meta.region }, bootstrapUrl);
|
|
1483
|
+
if (!stored.ok) {
|
|
1484
|
+
cancel(`Could not store the Owner setup link: ${stored.error}`);
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const setupLinkLine = setupLink === "print"
|
|
1489
|
+
? ` ${cyan(String(bootstrapUrl))}`
|
|
1490
|
+
: ` ${dim(`(shown in your Cloud Shell terminal; or: gcloud secrets versions access latest --secret=${INSTALL_SETUP_LINK_SECRET} --project=${meta.projectId})`)}`;
|
|
1283
1491
|
// Both providers report their records in `dnsRecordsRequiredOut`; the
|
|
1284
1492
|
// summary lists them in two groups so the main-domain records that take a
|
|
1285
1493
|
// live site offline are not set during the install (dns/groups.ts). On gcp
|
|
@@ -1319,7 +1527,7 @@ async function stepFinalize(installId) {
|
|
|
1319
1527
|
"",
|
|
1320
1528
|
bold("Next steps:"),
|
|
1321
1529
|
` 1. Owner setup — open in your browser (sign in with ${meta.ownerEmail}):`,
|
|
1322
|
-
|
|
1530
|
+
setupLinkLine,
|
|
1323
1531
|
` 2. AI provider — visit ${cyan(`${adminBase}/settings/ai`)} and paste your Anthropic API key`,
|
|
1324
1532
|
` (encrypted under the project KEK and stored in the database, no env vars).`,
|
|
1325
1533
|
` 3. Optional: connect Claude Code or another MCP client — at ${cyan(`${adminBase}/settings/mcp`)}`,
|