@caelo-cms/provisioning 0.10.44 → 0.10.47
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 +28 -0
- package/dist/adc-impersonation.d.ts.map +1 -0
- package/dist/adc-impersonation.js +148 -0
- package/dist/adc-impersonation.js.map +1 -0
- package/dist/checkpoint-secrets.d.ts.map +1 -1
- package/dist/checkpoint-secrets.js +2 -1
- package/dist/checkpoint-secrets.js.map +1 -1
- package/dist/cli.js +202 -33
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +45 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +112 -29
- package/dist/compose.js.map +1 -1
- package/dist/gcloud-retry.d.ts +5 -1
- package/dist/gcloud-retry.d.ts.map +1 -1
- package/dist/gcloud-retry.js.map +1 -1
- package/dist/gcloud.d.ts +2 -1
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +11 -13
- package/dist/gcloud.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +24 -0
- package/dist/infra-contract.d.ts.map +1 -0
- package/dist/infra-contract.js +25 -0
- package/dist/infra-contract.js.map +1 -0
- package/dist/install-state.d.ts +66 -2
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +162 -22
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts +6 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +189 -819
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +3 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +7 -5
- package/dist/migration-runner.js.map +1 -1
- package/dist/ops-backend.d.ts +46 -0
- package/dist/ops-backend.d.ts.map +1 -0
- package/dist/ops-backend.js +370 -0
- package/dist/ops-backend.js.map +1 -0
- package/dist/provisioner-identity.d.ts +103 -0
- package/dist/provisioner-identity.d.ts.map +1 -0
- package/dist/provisioner-identity.js +323 -0
- package/dist/provisioner-identity.js.map +1 -0
- package/dist/regions.d.ts.map +1 -1
- package/dist/regions.js +2 -1
- package/dist/regions.js.map +1 -1
- package/dist/release-contract.d.ts +37 -0
- package/dist/release-contract.d.ts.map +1 -0
- package/dist/release-contract.js +55 -0
- package/dist/release-contract.js.map +1 -0
- package/dist/release-manifest.d.ts +140 -0
- package/dist/release-manifest.d.ts.map +1 -0
- package/dist/release-manifest.js +319 -0
- package/dist/release-manifest.js.map +1 -0
- package/dist/self-command.d.ts +16 -0
- package/dist/self-command.d.ts.map +1 -0
- package/dist/self-command.js +20 -0
- package/dist/self-command.js.map +1 -0
- package/dist/self-hosted-release.d.ts +81 -0
- package/dist/self-hosted-release.d.ts.map +1 -0
- package/dist/self-hosted-release.js +148 -0
- package/dist/self-hosted-release.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +91 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -0
- package/dist/self-hosted-upgrade.js +355 -0
- package/dist/self-hosted-upgrade.js.map +1 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +2 -1
- package/dist/stack-converge.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 +114 -0
- package/dist/state-backend.d.ts.map +1 -0
- package/dist/state-backend.js +200 -0
- package/dist/state-backend.js.map +1 -0
- 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 +82 -0
- package/dist/state-store.d.ts.map +1 -0
- package/dist/state-store.js +205 -0
- package/dist/state-store.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +164 -0
- package/dist/upgrade/app-roll.d.ts.map +1 -0
- package/dist/upgrade/app-roll.js +270 -0
- package/dist/upgrade/app-roll.js.map +1 -0
- package/dist/upgrade/backup.d.ts +48 -0
- package/dist/upgrade/backup.d.ts.map +1 -0
- package/dist/upgrade/backup.js +71 -0
- package/dist/upgrade/backup.js.map +1 -0
- package/dist/upgrade/converge.d.ts +45 -0
- package/dist/upgrade/converge.d.ts.map +1 -0
- package/dist/upgrade/converge.js +287 -0
- package/dist/upgrade/converge.js.map +1 -0
- package/dist/upgrade/gcp-lookup.d.ts +23 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -0
- package/dist/upgrade/gcp-lookup.js +95 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -0
- package/dist/upgrade/health.d.ts +99 -0
- package/dist/upgrade/health.d.ts.map +1 -0
- package/dist/upgrade/health.js +214 -0
- package/dist/upgrade/health.js.map +1 -0
- package/dist/upgrade/release-images.d.ts +125 -0
- package/dist/upgrade/release-images.d.ts.map +1 -0
- package/dist/upgrade/release-images.js +315 -0
- package/dist/upgrade/release-images.js.map +1 -0
- package/dist/upgrade/reporter.d.ts +17 -0
- package/dist/upgrade/reporter.d.ts.map +1 -0
- package/dist/upgrade/reporter.js +28 -0
- package/dist/upgrade/reporter.js.map +1 -0
- package/dist/upgrade/upgrade.d.ts +79 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -0
- package/dist/upgrade/upgrade.js +105 -0
- package/dist/upgrade/upgrade.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +7 -3
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +7 -1
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +104 -8
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +159 -50
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +246 -78
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +4 -3
- package/stacks/self-hosted/README.md +4 -0
- package/stacks/self-hosted/index.ts +10 -2
package/dist/wizards/gcp.js
CHANGED
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* 5. Links billing
|
|
9
9
|
* 6. Enables 13 APIs in one call
|
|
10
10
|
* 7. Creates the provisioner SA + grants 15 IAM roles
|
|
11
|
-
* 8.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* 8. Lets the operator's gcloud account impersonate that SA (#696): no
|
|
12
|
+
* key is ever minted; an older install's `sa-key.json` is deleted,
|
|
13
|
+
* locally and in IAM (provisioner-identity.ts)
|
|
14
|
+
* 9. Pre-flight cost-estimate table; single y/N confirm
|
|
15
|
+
* 10. Ensures the install's ops bucket + KMS key in its own project and
|
|
16
|
+
* keeps Pulumi's state + install.json there (#696, ops-backend.ts).
|
|
17
|
+
* An older install whose state is on this machine keeps it there
|
|
18
|
+
* and its passphrase in secrets/pulumi-passphrase
|
|
14
19
|
* 11. Pulumi up via the Automation SDK; streams progress, then deletes
|
|
15
20
|
* the firewall rules GCP pre-populates on the default network
|
|
16
21
|
* 12. Prints DNS records + bootstrap URL + a note pointing the
|
|
@@ -25,6 +30,7 @@ import { homedir } from "node:os";
|
|
|
25
30
|
import { join } from "node:path";
|
|
26
31
|
import { cancel, confirm, isCancel, log, note, select, spinner, text } from "@clack/prompts";
|
|
27
32
|
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
33
|
+
import { checkAdcImpersonation } from "../adc-impersonation.js";
|
|
28
34
|
import { CERT_WAIT_TIMEOUT_MS, lbCertProbe, waitForCert } from "../cert-wait.js";
|
|
29
35
|
import { redactCredentials } from "../checkpoint-secrets.js";
|
|
30
36
|
import { removeDefaultFirewallRules } from "../default-firewall.js";
|
|
@@ -32,15 +38,19 @@ import { checkDeployedRuntimeEnv, chooseImageDigests, readDeployedImageDigests,
|
|
|
32
38
|
import { formatDnsRecordGroups, groupDnsRecords } from "../dns/groups.js";
|
|
33
39
|
import { pickDnsAdapter } from "../dns/index.js";
|
|
34
40
|
import { ensureGatewayRolePassword, findSqlInstance } from "../gateway-credentials.js";
|
|
35
|
-
import { activeAccount, createProject, createServiceAccount,
|
|
41
|
+
import { activeAccount, createProject, createServiceAccount, enableApis, gcloud, gcloudEnv, grantProvisionerRoles, linkBilling, listBillingAccounts, PROVISIONER_ROLE_LIST, projectExists, REQUIRED_API_LIST, serviceAccountExists, } from "../gcloud.js";
|
|
36
42
|
import { GCP_STACK_ENV } from "../gcp-names.js";
|
|
37
|
-
import { ensureInstallDir, installRoot, installStagingHost, isStepDone, markStepDone, readMetadata, readSecret, recordedImageDigests, recordImageDigests, writeMetadata, writeSecret, } from "../install-state.js";
|
|
43
|
+
import { adoptStateBackend, ensureInstallDir, installRoot, installStagingHost, isStepDone, markStepDone, readLocalMetadata, readMetadata, readSecret, recordedImageDigests, recordImageDigests, writeMetadata, writeSecret, } from "../install-state.js";
|
|
38
44
|
import { ensureOperatorAccessSync, installIapAllowlist, resolveOperatorAccessTarget, } from "../operator-access.js";
|
|
45
|
+
import { ensureOpsBackend } from "../ops-backend.js";
|
|
46
|
+
import { convergeKeylessProvisioner, PROVISIONER_ACCOUNT_ID, provisionerServiceAccountEmail, } from "../provisioner-identity.js";
|
|
39
47
|
import { decideRegion, detectCliRegion, installableRegions, suggestRegion } from "../regions.js";
|
|
40
|
-
import {
|
|
48
|
+
import { selfCommand } from "../self-command.js";
|
|
49
|
+
import { ABSENT_DEFAULT_FIREWALL_RULES } from "../stack-contract.js";
|
|
50
|
+
import { chooseInstallBackend, describeBackend, gcsBackendRecord, hasLocalPulumiState, installBackend, parseStateBackend, resolveBackend, } from "../state-backend.js";
|
|
51
|
+
import { acquireStateLock, GcsStateStore } from "../state-store.js";
|
|
41
52
|
import { estimateGcpCost } from "./gcp-cost.js";
|
|
42
|
-
import { pulumiUpGcp, readStackOutput } from "./gcp-pulumi.js";
|
|
43
|
-
const SA_ACCOUNT_ID = "caelo-provisioner";
|
|
53
|
+
import { GCP_STACK_KNOB_DEFAULTS, pulumiUpGcp, readStackOutput } from "./gcp-pulumi.js";
|
|
44
54
|
export async function runGcpWizard(opts) {
|
|
45
55
|
const { installId, domain, ownerEmail } = opts;
|
|
46
56
|
const { secretsDir } = ensureInstallDir(installId);
|
|
@@ -66,7 +76,7 @@ export async function runGcpWizard(opts) {
|
|
|
66
76
|
await stepBillingLink(installId, projectId, opts.nonInteractive);
|
|
67
77
|
// === 5. Enable APIs ===
|
|
68
78
|
await stepEnableApis(installId, projectId, opts.provider ?? "gcp");
|
|
69
|
-
// === 6. Service account + roles + key ===
|
|
79
|
+
// === 6. Service account + roles + impersonation (no key, #696) ===
|
|
70
80
|
// v0.3.12 — dropped the firebase-hosting-identity step. The
|
|
71
81
|
// Service Usage GenerateServiceIdentity API doesn't support
|
|
72
82
|
// `firebasehosting.googleapis.com` (returns
|
|
@@ -77,30 +87,17 @@ export async function runGcpWizard(opts) {
|
|
|
77
87
|
// PROJECT level (vs the Cloud Run service level) — project IAM
|
|
78
88
|
// accepts deferred google-managed principals; v1 Cloud Run IAM
|
|
79
89
|
// didn't.
|
|
80
|
-
const saEmail =
|
|
90
|
+
const saEmail = provisionerServiceAccountEmail(projectId);
|
|
81
91
|
await stepServiceAccount(installId, projectId, saEmail);
|
|
82
92
|
await stepGrantRoles(installId, projectId, saEmail);
|
|
83
|
-
|
|
84
|
-
// === 7. Pulumi passphrase ===
|
|
85
|
-
// v0.3.2 — removed the legacy `stepAnthropicKey` prompt. The
|
|
86
|
-
// runtime AI provider configuration lives at /settings/ai (the
|
|
87
|
-
// key is encrypted under the project KEK and stored in
|
|
88
|
-
// ai_providers). Pre-v0.3.2 the wizard captured a key + stored
|
|
89
|
-
// it in Secret Manager but the Cloud Run service never read it,
|
|
90
|
-
// so the prompt was dead UX cost.
|
|
91
|
-
const pulumiPassphrase = stepPulumiPassphrase(installId);
|
|
93
|
+
await stepKeylessProvisioner(projectId, secretsDir, opts.nonInteractive);
|
|
92
94
|
// === 8. Cost-estimate pre-flight ===
|
|
93
95
|
// v0.3.3 — provider variant threads through so gcp-firebase
|
|
94
96
|
// drops the LB / Cloud CDN / Cloud Armor lines (saves ~$19/mo
|
|
95
97
|
// vs gcp). Default 'gcp' for backwards compatibility with
|
|
96
98
|
// installs that don't set the field.
|
|
97
99
|
const costInputs = {
|
|
98
|
-
|
|
99
|
-
cloudSqlHa: false,
|
|
100
|
-
adminMinInstances: 0,
|
|
101
|
-
adminMemory: ADMIN_MEMORY_DEFAULT,
|
|
102
|
-
gatewayMinInstances: 0,
|
|
103
|
-
wafAdaptiveProtection: false,
|
|
100
|
+
...GCP_STACK_KNOB_DEFAULTS,
|
|
104
101
|
provider: opts.provider ?? "gcp",
|
|
105
102
|
region,
|
|
106
103
|
};
|
|
@@ -125,6 +122,11 @@ export async function runGcpWizard(opts) {
|
|
|
125
122
|
process.exit(0);
|
|
126
123
|
}
|
|
127
124
|
}
|
|
125
|
+
// === 8.5. Where Pulumi's state lives (#696) ===
|
|
126
|
+
// A new install: the ops bucket + KMS key in its own project — after the
|
|
127
|
+
// cost confirmation, which lists them (CLAUDE.md §11.C: nothing billable
|
|
128
|
+
// before the y/N). An older install keeps its local state + passphrase.
|
|
129
|
+
const backend = await stepStateBackend(installId, projectId, region, saEmail);
|
|
128
130
|
// === 9. Image digests to deploy ===
|
|
129
131
|
// An install that already runs a recorded release (from an earlier
|
|
130
132
|
// wizard run or from `upgrade`) keeps it: re-running the wizard converges
|
|
@@ -182,8 +184,8 @@ export async function runGcpWizard(opts) {
|
|
|
182
184
|
}),
|
|
183
185
|
ownerEmail,
|
|
184
186
|
region,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
impersonateServiceAccount: saEmail,
|
|
188
|
+
backend,
|
|
187
189
|
cloudSqlTier: costInputs.cloudSqlTier,
|
|
188
190
|
cloudSqlHa: costInputs.cloudSqlHa,
|
|
189
191
|
adminMinInstances: costInputs.adminMinInstances,
|
|
@@ -328,7 +330,7 @@ async function stepActiveGcloud() {
|
|
|
328
330
|
const account = await activeAccount();
|
|
329
331
|
if (!account) {
|
|
330
332
|
log.error(red("No active gcloud account."));
|
|
331
|
-
log.warn(`Run ${bold("gcloud auth login")} in another terminal, then re-run ${bold(
|
|
333
|
+
log.warn(`Run ${bold("gcloud auth login")} in another terminal, then re-run ${bold(selfCommand())}.`);
|
|
332
334
|
cancel("Aborted — no gcloud auth.");
|
|
333
335
|
process.exit(2);
|
|
334
336
|
}
|
|
@@ -473,7 +475,7 @@ async function stepBillingLink(installId, projectId, nonInteractive) {
|
|
|
473
475
|
const open = accounts.filter((a) => a.open);
|
|
474
476
|
if (open.length === 0) {
|
|
475
477
|
log.error(red("No open billing accounts found."));
|
|
476
|
-
log.warn(`Open one at https://console.cloud.google.com/billing then re-run ${bold(
|
|
478
|
+
log.warn(`Open one at https://console.cloud.google.com/billing then re-run ${bold(selfCommand())}.`);
|
|
477
479
|
cancel("Aborted.");
|
|
478
480
|
process.exit(1);
|
|
479
481
|
}
|
|
@@ -543,7 +545,7 @@ async function stepEnableApis(installId, projectId, provider = "gcp") {
|
|
|
543
545
|
markStepDone(installId, stepName, { count: apiCount });
|
|
544
546
|
}
|
|
545
547
|
async function stepServiceAccount(installId, projectId, saEmail) {
|
|
546
|
-
const stepName = `service-account-${
|
|
548
|
+
const stepName = `service-account-${PROVISIONER_ACCOUNT_ID}`;
|
|
547
549
|
if (isStepDone(installId, stepName)) {
|
|
548
550
|
log.success(`Provisioner SA ${dim("(already created)")}`);
|
|
549
551
|
return;
|
|
@@ -555,7 +557,7 @@ async function stepServiceAccount(installId, projectId, saEmail) {
|
|
|
555
557
|
}
|
|
556
558
|
const s = spinner();
|
|
557
559
|
s.start(`Creating provisioner service account...`);
|
|
558
|
-
const r = await createServiceAccount(projectId,
|
|
560
|
+
const r = await createServiceAccount(projectId, PROVISIONER_ACCOUNT_ID, "Caelo provisioner");
|
|
559
561
|
if (!r.ok) {
|
|
560
562
|
s.stop(red(`Failed: ${r.stderr.trim()}`));
|
|
561
563
|
cancel("Aborted.");
|
|
@@ -658,24 +660,147 @@ async function stepRemoveDefaultFirewallRules(projectId) {
|
|
|
658
660
|
for (const o of failed)
|
|
659
661
|
log.warn(yellow(` not removed: ${o.id} — ${o.why}\n ${o.error ?? ""}`));
|
|
660
662
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
663
|
+
/**
|
|
664
|
+
* #696 — make the provisioner SA usable without a key: the operator's gcloud
|
|
665
|
+
* account gets TokenCreator on it, an older install's `sa-key.json` is
|
|
666
|
+
* deleted (locally and in IAM), and the credential chain Pulumi will use —
|
|
667
|
+
* Application Default Credentials impersonating the SA — is verified before
|
|
668
|
+
* anything billable runs. Idempotent; runs on every wizard pass.
|
|
669
|
+
*/
|
|
670
|
+
async function stepKeylessProvisioner(projectId, secretsDir, nonInteractive) {
|
|
671
|
+
const sa = provisionerServiceAccountEmail(projectId);
|
|
672
|
+
const account = await activeAccount();
|
|
673
|
+
if (!account) {
|
|
674
|
+
cancel("No active gcloud account. Run `gcloud auth login`, then re-run.");
|
|
675
|
+
process.exit(2);
|
|
667
676
|
}
|
|
668
677
|
const s = spinner();
|
|
669
|
-
s.start(`
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
678
|
+
s.start(`Letting ${account} impersonate ${sa} (no key file)...`);
|
|
679
|
+
const converged = await convergeKeylessProvisioner({
|
|
680
|
+
projectId,
|
|
681
|
+
secretsDir,
|
|
682
|
+
operatorAccount: account,
|
|
683
|
+
justCreated: true,
|
|
684
|
+
});
|
|
685
|
+
if (!converged.ok) {
|
|
686
|
+
s.stop(red("Could not set up impersonation of the provisioner"));
|
|
687
|
+
log.error(converged.error);
|
|
688
|
+
cancel("Aborted.");
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
if (converged.grant === "no-provisioner") {
|
|
692
|
+
// stepServiceAccount ensured it and the lookup waited out propagation.
|
|
693
|
+
s.stop(red(`${sa} does not exist`));
|
|
694
|
+
cancel("Aborted — re-run the installer.");
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
s.stop(green(converged.grant === "granted"
|
|
698
|
+
? `${account} may now impersonate ${sa}`
|
|
699
|
+
: `${account} may impersonate ${sa} ${dim("(already)")}`));
|
|
700
|
+
if (converged.legacyKey === "removed") {
|
|
701
|
+
log.warn(yellow("Removed the provisioner's long-lived key; this install now uses impersonation."));
|
|
702
|
+
for (const line of converged.lines)
|
|
703
|
+
log.info(` ${line}`);
|
|
704
|
+
}
|
|
705
|
+
const check = async () => checkAdcImpersonation({
|
|
706
|
+
projectId,
|
|
707
|
+
operatorAccount: account,
|
|
708
|
+
freshGrant: converged.grant === "granted",
|
|
709
|
+
});
|
|
710
|
+
const sAdc = spinner();
|
|
711
|
+
sAdc.start("Checking that your Application Default Credentials can impersonate it...");
|
|
712
|
+
let adc = await check();
|
|
713
|
+
if (!adc.ok && adc.reason === "no-adc" && !nonInteractive) {
|
|
714
|
+
sAdc.stop(yellow("No Application Default Credentials yet"));
|
|
715
|
+
const login = await confirm({
|
|
716
|
+
message: `Run ${bold("gcloud auth application-default login")} now (opens your browser; sign in as ${account})?`,
|
|
717
|
+
initialValue: true,
|
|
718
|
+
});
|
|
719
|
+
if (isCancel(login) || !login) {
|
|
720
|
+
log.error(adc.error);
|
|
721
|
+
cancel("Aborted.");
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
const { spawnSync } = await import("node:child_process");
|
|
725
|
+
spawnSync("gcloud", ["auth", "application-default", "login"], { stdio: "inherit" });
|
|
726
|
+
sAdc.start("Checking again...");
|
|
727
|
+
adc = await check();
|
|
728
|
+
}
|
|
729
|
+
if (!adc.ok) {
|
|
730
|
+
sAdc.stop(red("Infrastructure credentials not ready"));
|
|
731
|
+
log.error(adc.error);
|
|
673
732
|
cancel("Aborted.");
|
|
674
733
|
process.exit(1);
|
|
675
734
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
735
|
+
sAdc.stop(green(`Pulumi will run as ${sa} via impersonation`));
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Decide and prepare where the install's Pulumi state lives (#696,
|
|
739
|
+
* state-backend.ts chooseInstallBackend) and resolve it for Pulumi.
|
|
740
|
+
*
|
|
741
|
+
* For the ops bucket: ensures bucket + KMS key + IAM (idempotent, every
|
|
742
|
+
* run), then records the backend in install.json and moves install.json +
|
|
743
|
+
* progress.json into the bucket. A project whose bucket already holds
|
|
744
|
+
* another install's install.json is refused — this machine would overwrite
|
|
745
|
+
* that install's record; this install's own copy from an interrupted run is
|
|
746
|
+
* taken over.
|
|
747
|
+
*/
|
|
748
|
+
async function stepStateBackend(installId, projectId, region, provisionerEmail) {
|
|
749
|
+
const root = installRoot(installId);
|
|
750
|
+
const meta = readMetadata(installId);
|
|
751
|
+
if (!meta)
|
|
752
|
+
throw new Error(`No install.json for install '${installId}'.`);
|
|
753
|
+
const kind = chooseInstallBackend(meta, hasLocalPulumiState(root), isStepDone(installId, `pulumi-up-${projectId}`));
|
|
754
|
+
if (kind === "local") {
|
|
755
|
+
stepPulumiPassphrase(installId);
|
|
756
|
+
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
|
+
return resolveBackend(meta, {
|
|
758
|
+
installRoot: root,
|
|
759
|
+
readPassphrase: () => readSecret(installId, "pulumi-passphrase"),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const account = await activeAccount();
|
|
763
|
+
if (!account) {
|
|
764
|
+
cancel("No active gcloud account. Run `gcloud auth login`, then re-run.");
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const s = spinner();
|
|
768
|
+
s.start(`Ensuring the install's state bucket + KMS key in ${projectId}...`);
|
|
769
|
+
const r = await ensureOpsBackend({
|
|
770
|
+
projectId,
|
|
771
|
+
region,
|
|
772
|
+
operatorAccount: account,
|
|
773
|
+
provisionerEmail,
|
|
774
|
+
});
|
|
775
|
+
if (!r.ok) {
|
|
776
|
+
s.stop(red("Could not set up where the install's state lives"));
|
|
777
|
+
log.error(r.error);
|
|
778
|
+
cancel("Aborted before anything else was provisioned; fix the above and re-run.");
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
s.stop(green(r.actions.length > 0
|
|
782
|
+
? `State bucket + KMS key ready ${dim(`(${r.actions.length} change(s))`)}`
|
|
783
|
+
: `State bucket + KMS key ok ${dim("(already in place)")}`));
|
|
784
|
+
for (const a of r.actions)
|
|
785
|
+
log.info(dim(` ${a}`));
|
|
786
|
+
if (meta.stateBackend?.kind !== "gcs") {
|
|
787
|
+
const record = gcsBackendRecord(projectId, region);
|
|
788
|
+
const existing = new GcsStateStore(record.bucket, root).peek("install.json");
|
|
789
|
+
const other = existing === null ? null : JSON.parse(existing);
|
|
790
|
+
// The same install (id + creation time) is this machine's own earlier
|
|
791
|
+
// run, interrupted between writing the bucket and the local copy:
|
|
792
|
+
// finishing the move is safe. Anything else is another install.
|
|
793
|
+
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. To manage it from this machine run: ${selfCommand("attach", "--project", projectId)}`);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
adoptStateBackend(installId, record);
|
|
798
|
+
log.success(`Pulumi state + install metadata live in ${bold(describeBackend({ stateBackend: record }, root))}`);
|
|
799
|
+
}
|
|
800
|
+
const current = readMetadata(installId);
|
|
801
|
+
if (!current)
|
|
802
|
+
throw new Error(`install.json for '${installId}' vanished from its store.`);
|
|
803
|
+
return resolveBackend(current, { installRoot: root, readPassphrase: () => null });
|
|
679
804
|
}
|
|
680
805
|
function stepPulumiPassphrase(installId) {
|
|
681
806
|
const existing = readSecret(installId, "pulumi-passphrase");
|
|
@@ -690,33 +815,77 @@ function stepPulumiPassphrase(installId) {
|
|
|
690
815
|
}
|
|
691
816
|
async function stepPulumiUp(installId, opts) {
|
|
692
817
|
const stepName = `pulumi-up-${opts.projectId}`;
|
|
693
|
-
|
|
818
|
+
const deployed = isStepDone(installId, stepName);
|
|
819
|
+
if (deployed) {
|
|
694
820
|
log.success(`Pulumi up ${dim("(checkpointed — re-running for drift refresh)")}`);
|
|
695
821
|
}
|
|
696
822
|
const { secretsDir } = ensureInstallDir(installId);
|
|
697
823
|
const root = installRoot(installId);
|
|
824
|
+
// #696 — hold the install's state lock through `pulumi up` (`state
|
|
825
|
+
// migrate` takes the same one), then re-check, uncached, that the state
|
|
826
|
+
// still lives where this run resolved it: the bucket's install.json for a
|
|
827
|
+
// bucket-backed install (another machine may have moved it back), the
|
|
828
|
+
// file on disk for a local one.
|
|
829
|
+
const opsBucket = gcsBackendRecord(opts.projectId, opts.region).bucket;
|
|
830
|
+
let releaseLock;
|
|
831
|
+
try {
|
|
832
|
+
releaseLock = acquireStateLock(opts.backend.kind === "gcs" ? { kind: "gcs", bucket: opsBucket } : { kind: "local" }, root, "installer pulumi up");
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
cancel(e instanceof Error ? e.message : String(e));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
let moved;
|
|
839
|
+
try {
|
|
840
|
+
const nowText = opts.backend.kind === "gcs"
|
|
841
|
+
? new GcsStateStore(opsBucket, root).peek("install.json")
|
|
842
|
+
: JSON.stringify(readLocalMetadata(installId));
|
|
843
|
+
const now = nowText ? JSON.parse(nowText) : null;
|
|
844
|
+
moved =
|
|
845
|
+
!now ||
|
|
846
|
+
installBackend({ stateBackend: parseStateBackend(now.stateBackend) }).kind !==
|
|
847
|
+
opts.backend.kind;
|
|
848
|
+
}
|
|
849
|
+
catch (e) {
|
|
850
|
+
releaseLock();
|
|
851
|
+
throw e;
|
|
852
|
+
}
|
|
853
|
+
if (moved) {
|
|
854
|
+
releaseLock();
|
|
855
|
+
cancel("The install's Pulumi state has moved (state migrate) since this run started. Re-run the installer.");
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
698
858
|
log.info(`Pulumi up — wall-clock 8–15 min (Cloud SQL is the long pole). Streaming progress...`);
|
|
699
859
|
let resourceCount = 0;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
860
|
+
let result;
|
|
861
|
+
try {
|
|
862
|
+
result = await pulumiUpGcp({
|
|
863
|
+
installId,
|
|
864
|
+
installRoot: root,
|
|
865
|
+
secretsDir,
|
|
866
|
+
...opts,
|
|
867
|
+
// A deployed install's stack must exist: a missing one means the
|
|
868
|
+
// state is elsewhere, and a fresh stack would try to recreate it all.
|
|
869
|
+
stackMustExist: deployed,
|
|
870
|
+
provider: opts.provider ?? "gcp",
|
|
871
|
+
}, (kind, message) => {
|
|
872
|
+
if (kind === "resource") {
|
|
873
|
+
resourceCount++;
|
|
874
|
+
if (resourceCount % 5 === 0) {
|
|
875
|
+
process.stdout.write(`\r${dim(` ${resourceCount} resources updated`)}`);
|
|
876
|
+
}
|
|
711
877
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
878
|
+
else if (kind === "error") {
|
|
879
|
+
process.stdout.write("\n");
|
|
880
|
+
log.error(red(redactCredentials(message)));
|
|
881
|
+
}
|
|
882
|
+
// logs: silent — too noisy for a live progress UI; pulumi
|
|
883
|
+
// diagnostics will print on failure via the SDK's onOutput.
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
finally {
|
|
887
|
+
releaseLock();
|
|
888
|
+
}
|
|
720
889
|
process.stdout.write("\n");
|
|
721
890
|
log.success(green(`Pulumi up complete — ${result.resourceCount.created} created, ${result.resourceCount.updated} updated, ${result.resourceCount.deleted} deleted`));
|
|
722
891
|
markStepDone(installId, stepName, { outputs: result.outputs });
|
|
@@ -1068,13 +1237,14 @@ async function stepUploadStaticPlaceholder(installId, projectId, domain) {
|
|
|
1068
1237
|
* re-run picks it up.
|
|
1069
1238
|
*/
|
|
1070
1239
|
async function readBootstrapUrl(installId, provider) {
|
|
1071
|
-
const
|
|
1072
|
-
if (!
|
|
1073
|
-
throw new Error(
|
|
1074
|
-
}
|
|
1240
|
+
const meta = readMetadata(installId);
|
|
1241
|
+
if (!meta)
|
|
1242
|
+
throw new Error(`No install.json for install '${installId}'.`);
|
|
1075
1243
|
const value = await readStackOutput({
|
|
1076
|
-
|
|
1077
|
-
|
|
1244
|
+
backend: resolveBackend(meta, {
|
|
1245
|
+
installRoot: installRoot(installId),
|
|
1246
|
+
readPassphrase: () => readSecret(installId, "pulumi-passphrase"),
|
|
1247
|
+
}),
|
|
1078
1248
|
provider: provider === "gcp-firebase" ? "gcp-firebase" : "gcp",
|
|
1079
1249
|
name: "bootstrapUrlOut",
|
|
1080
1250
|
});
|
|
@@ -1127,15 +1297,13 @@ async function stepGcpDnsRecords(installId) {
|
|
|
1127
1297
|
}
|
|
1128
1298
|
catch (e) {
|
|
1129
1299
|
log.warn(yellow(`DNS records not confirmed yet: ${e instanceof Error ? e.message : String(e)}`));
|
|
1130
|
-
log.warn(`Set them at your registrar, then re-run ${bold(
|
|
1300
|
+
log.warn(`Set them at your registrar, then re-run ${bold(selfCommand())} to resume.`);
|
|
1131
1301
|
cancel("Stopped at the DNS step.");
|
|
1132
1302
|
process.exit(1);
|
|
1133
1303
|
}
|
|
1134
1304
|
}
|
|
1135
1305
|
async function stepFinalize(installId) {
|
|
1136
1306
|
const meta = readMetadata(installId);
|
|
1137
|
-
const progress = readSecret(installId, "pulumi-passphrase"); // touch to verify state still present
|
|
1138
|
-
void progress;
|
|
1139
1307
|
if (!meta?.projectId)
|
|
1140
1308
|
return;
|
|
1141
1309
|
const outputs = await recordedStackOutputs(installId, meta.projectId);
|
|
@@ -1179,12 +1347,12 @@ async function stepFinalize(installId) {
|
|
|
1179
1347
|
bold("Admin custom domain (admin.<domain>):"),
|
|
1180
1348
|
` Not configured — using the Cloud Run URL above.`,
|
|
1181
1349
|
` To bind ${cyan(`admin.${meta.domain}`)}, run`,
|
|
1182
|
-
` ${dim("
|
|
1350
|
+
` ${dim(selfCommand("admin-domain", "enable"))}`,
|
|
1183
1351
|
` (it walks you through the one-time Search Console verification, prints the DNS record and waits for the certificate).`,
|
|
1184
1352
|
"",
|
|
1185
1353
|
bold("Protected staging site:"),
|
|
1186
1354
|
` Stage builds render ${cyan(installStagingHost(meta))}; to serve it, run`,
|
|
1187
|
-
` ${dim("
|
|
1355
|
+
` ${dim(selfCommand("staging-domain", "enable"))}`,
|
|
1188
1356
|
` It maps the host, turns on password protection and prints the staging login once;`,
|
|
1189
1357
|
` add the DNS record it prints. The host serves pages after the first Stage.`,
|
|
1190
1358
|
]
|
|
@@ -1206,9 +1374,9 @@ async function stepFinalize(installId) {
|
|
|
1206
1374
|
...adminNote,
|
|
1207
1375
|
"",
|
|
1208
1376
|
bold("Lifecycle commands:"),
|
|
1209
|
-
` ${dim("
|
|
1210
|
-
` ${dim("
|
|
1211
|
-
` ${
|
|
1377
|
+
` ${dim(selfCommand("status"))} — health check, and the exact upgrade command once a new release is out`,
|
|
1378
|
+
` ${dim(selfCommand("destroy"))} — tear everything down (irreversible)`,
|
|
1379
|
+
` New releases: ${cyan(`${adminBase}/settings/updates`)} shows the exact upgrade command for this install.`,
|
|
1212
1380
|
].join("\n"), "Done");
|
|
1213
1381
|
void homedir; // unused-import guard
|
|
1214
1382
|
void readFileSync;
|