@caelo-cms/provisioning 0.10.61 → 0.10.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/caddy/Caddyfile.production +7 -0
- package/caddy/Caddyfile.staging +7 -0
- package/dist/admin-internal-answer.d.ts +12 -0
- package/dist/admin-internal-answer.d.ts.map +1 -0
- package/dist/admin-internal-answer.js +32 -0
- package/dist/admin-internal-answer.js.map +1 -0
- package/dist/admin-internal-google.d.ts +4 -0
- package/dist/admin-internal-google.d.ts.map +1 -0
- package/dist/admin-internal-google.js +161 -0
- package/dist/admin-internal-google.js.map +1 -0
- package/dist/admin-internal-self-hosted.d.ts +30 -0
- package/dist/admin-internal-self-hosted.d.ts.map +1 -0
- package/dist/admin-internal-self-hosted.js +92 -0
- package/dist/admin-internal-self-hosted.js.map +1 -0
- package/dist/admin-internal.d.ts +33 -12
- package/dist/admin-internal.d.ts.map +1 -1
- package/dist/admin-internal.js +27 -159
- package/dist/admin-internal.js.map +1 -1
- package/dist/agent/command.d.ts +7 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -0
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/lock.d.ts +13 -1
- package/dist/agent/lock.d.ts.map +1 -1
- package/dist/agent/lock.js +62 -5
- package/dist/agent/lock.js.map +1 -1
- package/dist/agent/run.d.ts +5 -1
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +8 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +192 -90
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +11 -1
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +95 -38
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +7 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +10 -7
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +21 -0
- package/dist/converge-job-preflight.d.ts.map +1 -0
- package/dist/converge-job-preflight.js +111 -0
- package/dist/converge-job-preflight.js.map +1 -0
- package/dist/converge-job.d.ts +8 -2
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +14 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/gateway-credentials.d.ts +5 -3
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +5 -8
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +29 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +66 -0
- package/dist/gcloud.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +10 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +3 -1
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +10 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +12 -0
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +113 -28
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-iap.d.ts +2 -0
- package/dist/mcp-iap.d.ts.map +1 -1
- package/dist/mcp-iap.js +50 -17
- package/dist/mcp-iap.js.map +1 -1
- package/dist/owner-reset.d.ts +16 -19
- package/dist/owner-reset.d.ts.map +1 -1
- package/dist/owner-reset.js +92 -19
- package/dist/owner-reset.js.map +1 -1
- package/dist/preview-plan.d.ts +71 -0
- package/dist/preview-plan.d.ts.map +1 -0
- package/dist/preview-plan.js +168 -0
- package/dist/preview-plan.js.map +1 -0
- package/dist/public-role-isolation.d.ts +2 -0
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +9 -3
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +4 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/self-hosted-internal-secret.d.ts +27 -0
- package/dist/self-hosted-internal-secret.d.ts.map +1 -0
- package/dist/self-hosted-internal-secret.js +69 -0
- package/dist/self-hosted-internal-secret.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -2
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +13 -1
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +26 -2
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-converge.d.ts +5 -2
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +73 -8
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +4 -1
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js.map +1 -1
- package/dist/state-backend.d.ts +3 -2
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +3 -2
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +17 -0
- package/dist/state-command.d.ts.map +1 -1
- package/dist/state-command.js +34 -5
- package/dist/state-command.js.map +1 -1
- package/dist/state-migrate.d.ts +14 -3
- package/dist/state-migrate.d.ts.map +1 -1
- package/dist/state-migrate.js +31 -7
- package/dist/state-migrate.js.map +1 -1
- package/dist/state-pulumi.d.ts +0 -2
- package/dist/state-pulumi.d.ts.map +1 -1
- package/dist/state-pulumi.js +19 -20
- package/dist/state-pulumi.js.map +1 -1
- package/dist/static-security-headers.d.ts +86 -0
- package/dist/static-security-headers.d.ts.map +1 -0
- package/dist/static-security-headers.js +126 -0
- package/dist/static-security-headers.js.map +1 -0
- package/dist/static-svg-gcp.d.ts +28 -0
- package/dist/static-svg-gcp.d.ts.map +1 -0
- package/dist/static-svg-gcp.js +106 -0
- package/dist/static-svg-gcp.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +3 -0
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +12 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/backup.d.ts +2 -0
- package/dist/upgrade/backup.d.ts.map +1 -1
- package/dist/upgrade/backup.js +5 -4
- package/dist/upgrade/backup.js.map +1 -1
- package/dist/upgrade/converge.d.ts +7 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +19 -7
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +38 -3
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +31 -9
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -5
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +114 -25
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +17 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +23 -12
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/reporter.d.ts +2 -0
- package/dist/upgrade/reporter.d.ts.map +1 -1
- package/dist/upgrade/reporter.js +1 -0
- package/dist/upgrade/reporter.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +11 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +5 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -2
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +22 -13
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +16 -3
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +9 -4
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +10 -8
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +1 -0
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +70 -23
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +118 -39
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +67 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-internal-answer.ts +43 -0
- package/src/admin-internal-google.ts +187 -0
- package/src/admin-internal-self-hosted.test.ts +70 -0
- package/src/admin-internal-self-hosted.ts +128 -0
- package/src/admin-internal.ts +59 -174
- package/src/admin-memory.test.ts +1 -0
- package/src/agent/command.ts +3 -0
- package/src/agent/lock-config.test.ts +57 -2
- package/src/agent/lock.ts +68 -5
- package/src/agent/run.test.ts +18 -1
- package/src/agent/run.ts +17 -6
- package/src/caddy-redirects.test.ts +1 -0
- package/src/caddy.ts +12 -0
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +102 -44
- package/src/compose.ts +12 -0
- package/src/converge-job-gcp.ts +16 -11
- package/src/converge-job-preflight.test.ts +108 -0
- package/src/converge-job-preflight.ts +124 -0
- package/src/converge-job.test.ts +50 -0
- package/src/converge-job.ts +38 -2
- package/src/gateway-credentials.test.ts +4 -3
- package/src/gateway-credentials.ts +6 -10
- package/src/gateway-env-isolation.test.ts +1 -0
- package/src/gcloud.test.ts +93 -1
- package/src/gcloud.ts +82 -0
- package/src/index.test.ts +38 -1
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +10 -1
- package/src/install-job.ts +5 -2
- package/src/install-state.ts +13 -0
- package/src/lifecycle.ts +134 -28
- package/src/mcp-iap.test.ts +66 -5
- package/src/mcp-iap.ts +55 -17
- package/src/owner-reset-providers.test.ts +370 -0
- package/src/owner-reset.test.ts +2 -2
- package/src/owner-reset.ts +108 -19
- package/src/preview-plan.test.ts +233 -0
- package/src/preview-plan.ts +226 -0
- package/src/public-role-isolation.test.ts +13 -2
- package/src/public-role-isolation.ts +14 -4
- package/src/release-contract.ts +4 -0
- package/src/self-hosted-compose.test.ts +25 -0
- package/src/self-hosted-internal-secret.test.ts +61 -0
- package/src/self-hosted-internal-secret.ts +80 -0
- package/src/self-hosted-release.ts +10 -3
- package/src/self-hosted-upgrade.test.ts +35 -1
- package/src/self-hosted-upgrade.ts +40 -3
- package/src/stack-contract.test.ts +1 -0
- package/src/stack-converge.test.ts +89 -5
- package/src/stack-converge.ts +81 -8
- package/src/staging-admin.test.ts +1 -1
- package/src/staging-admin.ts +2 -1
- package/src/state-backend.ts +3 -2
- package/src/state-command.test.ts +31 -0
- package/src/state-command.ts +35 -8
- package/src/state-migrate.test.ts +58 -7
- package/src/state-migrate.ts +48 -10
- package/src/state-pulumi.ts +20 -21
- package/src/static-security-headers.test.ts +124 -0
- package/src/static-security-headers.ts +154 -0
- package/src/static-svg-gcp.test.ts +133 -0
- package/src/static-svg-gcp.ts +140 -0
- package/src/upgrade/app-roll.ts +18 -7
- package/src/upgrade/backup.ts +14 -5
- package/src/upgrade/converge.ts +25 -7
- package/src/upgrade/gcp-lookup.test.ts +80 -1
- package/src/upgrade/gcp-lookup.ts +61 -11
- package/src/upgrade/health.test.ts +132 -8
- package/src/upgrade/health.ts +169 -31
- package/src/upgrade/release-images.test.ts +17 -0
- package/src/upgrade/release-images.ts +26 -14
- package/src/upgrade/reporter.ts +3 -0
- package/src/upgrade/upgrade.test.ts +137 -8
- package/src/upgrade/upgrade.ts +14 -2
- package/src/upgrader-gcp.ts +28 -13
- package/src/upgrader-iam-preflight.test.ts +6 -3
- package/src/upgrader-iam-preflight.ts +26 -5
- package/src/upgrader-job.ts +14 -6
- package/src/upgrader-reporter.ts +1 -0
- package/src/wizards/gcp-pulumi.test.ts +1 -1
- package/src/wizards/gcp-pulumi.ts +147 -38
- package/src/wizards/gcp.ts +89 -9
- package/src/wizards/stack-work-dir.test.ts +175 -0
- package/stacks/aws/index.ts +40 -0
- package/stacks/azure/index.ts +22 -1
- package/stacks/gcp/README.md +4 -0
- package/stacks/gcp/index.ts +6 -2
- package/stacks/gcp/Pulumi.production.yaml +0 -16
package/src/stack-converge.ts
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
* - {@link planEnvUpdate}: the env-var flags for the `services update` that
|
|
9
9
|
* rolls the image, so env changes ride the same new revision — including
|
|
10
10
|
* moving a plain var to a Secret Manager reference.
|
|
11
|
-
* - {@link ensureStackInvariants}: IAM bindings + CDN settings.
|
|
11
|
+
* - {@link ensureStackInvariants}: IAM bindings + CDN settings (incl. the
|
|
12
|
+
* #891 response headers and the SVG backfill, static-svg-gcp.ts). Each binding
|
|
12
13
|
* is checked against the resource's policy first and only added when
|
|
13
14
|
* missing, so an operator without IAM-admin rights can still upgrade an
|
|
14
15
|
* install that is already in shape.
|
|
15
16
|
* - {@link ensureStackInvariants} also deletes the firewall rules GCP
|
|
16
17
|
* pre-populates on the `default` network (SSH, RDP, ICMP, internal)
|
|
17
18
|
* (default-firewall.ts), the one thing it removes.
|
|
19
|
+
* - {@link ensureStackInvariants} opens a gcp-firebase gateway created
|
|
20
|
+
* load-balancer-only to all traffic (#650, #732), before migrations:
|
|
21
|
+
* the upgrade's own health check reaches it on its run.app URL.
|
|
18
22
|
* - {@link planMediaVolume}: the admin's media bucket volume (gen2 + Cloud
|
|
19
23
|
* Storage volume + mount), added in the same `services update` when
|
|
20
24
|
* missing.
|
|
@@ -55,6 +59,8 @@ import {
|
|
|
55
59
|
stackIamInvariants,
|
|
56
60
|
staticBackendBucketPrefix,
|
|
57
61
|
} from "./stack-contract.js";
|
|
62
|
+
import { GCP_STATIC_RESPONSE_HEADERS } from "./static-security-headers.js";
|
|
63
|
+
import { ensureSvgDownloads } from "./static-svg-gcp.js";
|
|
58
64
|
|
|
59
65
|
// ===========================================================================
|
|
60
66
|
// Env vars
|
|
@@ -517,7 +523,10 @@ async function ensureIam(
|
|
|
517
523
|
return outcomes;
|
|
518
524
|
}
|
|
519
525
|
|
|
520
|
-
/**
|
|
526
|
+
/**
|
|
527
|
+
* Bring the `gcp` stack's static backend bucket to {@link STATIC_CDN_POLICY}
|
|
528
|
+
* and its custom response headers to {@link GCP_STATIC_RESPONSE_HEADERS}.
|
|
529
|
+
*/
|
|
521
530
|
async function ensureCdnPolicy(
|
|
522
531
|
install: InstallTarget,
|
|
523
532
|
run: GcloudRunner,
|
|
@@ -525,7 +534,7 @@ async function ensureCdnPolicy(
|
|
|
525
534
|
const base = {
|
|
526
535
|
id: `Cloud CDN policy of ${staticBackendBucketPrefix(install.env)}*`,
|
|
527
536
|
onFailure: "warn" as const,
|
|
528
|
-
why: "immutable assets stay cached for a year instead of 1h (#555)",
|
|
537
|
+
why: "immutable assets stay cached for a year instead of 1h (#555), and every file is served nosniff (#891)",
|
|
529
538
|
};
|
|
530
539
|
const project = `--project=${install.projectId}`;
|
|
531
540
|
const list = await run([
|
|
@@ -553,12 +562,18 @@ async function ensureCdnPolicy(
|
|
|
553
562
|
"--format=json",
|
|
554
563
|
]);
|
|
555
564
|
if (describe.ok) {
|
|
556
|
-
const
|
|
565
|
+
const live = JSON.parse(describe.stdout) as {
|
|
566
|
+
cdnPolicy?: Record<string, unknown>;
|
|
567
|
+
customResponseHeaders?: unknown;
|
|
568
|
+
};
|
|
569
|
+
const cdn = live.cdnPolicy;
|
|
557
570
|
const inShape =
|
|
558
571
|
cdn !== undefined &&
|
|
559
572
|
(Object.keys(STATIC_CDN_POLICY) as (keyof typeof STATIC_CDN_POLICY)[]).every(
|
|
560
573
|
(k) => String(cdn[k]) === String(STATIC_CDN_POLICY[k]),
|
|
561
|
-
)
|
|
574
|
+
) &&
|
|
575
|
+
JSON.stringify(live.customResponseHeaders ?? []) ===
|
|
576
|
+
JSON.stringify(GCP_STATIC_RESPONSE_HEADERS);
|
|
562
577
|
if (inShape) return { ...base, status: "present" };
|
|
563
578
|
}
|
|
564
579
|
const update = await run([
|
|
@@ -571,6 +586,8 @@ async function ensureCdnPolicy(
|
|
|
571
586
|
`--default-ttl=${STATIC_CDN_POLICY.defaultTtl}`,
|
|
572
587
|
`--max-ttl=${STATIC_CDN_POLICY.maxTtl}`,
|
|
573
588
|
`--client-ttl=${STATIC_CDN_POLICY.clientTtl}`,
|
|
589
|
+
// Replaces the bucket's list, so this is the complete set.
|
|
590
|
+
...GCP_STATIC_RESPONSE_HEADERS.map((h) => `--custom-response-header=${h}`),
|
|
574
591
|
"--quiet",
|
|
575
592
|
]);
|
|
576
593
|
return update.ok
|
|
@@ -578,10 +595,62 @@ async function ensureCdnPolicy(
|
|
|
578
595
|
: { ...base, status: "failed", error: update.stderr.trim() };
|
|
579
596
|
}
|
|
580
597
|
|
|
598
|
+
/** The ingress of a described service (`all`, `internal-and-cloud-load-balancing`, …; null: unset). */
|
|
599
|
+
export function liveIngress(serviceJson: string): string | null {
|
|
600
|
+
const svc = JSON.parse(serviceJson) as {
|
|
601
|
+
metadata?: { annotations?: Record<string, string> };
|
|
602
|
+
};
|
|
603
|
+
return svc.metadata?.annotations?.["run.googleapis.com/ingress"] ?? null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Open a gcp-firebase gateway to all traffic, as the stack declares it
|
|
608
|
+
* (`INGRESS_TRAFFIC_ALL` since #650). There is no load balancer on
|
|
609
|
+
* gcp-firebase: the staging domain mapping, remote-MCP clients and the
|
|
610
|
+
* upgrade's own health probe reach the gateway's run.app URL. A gateway
|
|
611
|
+
* created before #650 kept `internal-and-cloud-load-balancing` (only
|
|
612
|
+
* `pulumi up` would change it), answered those requests with Google's HTML
|
|
613
|
+
* 404, and every upgrade rolled back after its migrations (#732).
|
|
614
|
+
*
|
|
615
|
+
* Ingress is a service-level setting: the update creates no revision, so
|
|
616
|
+
* the serving one and the app-roll's rollback target stay as they are. On
|
|
617
|
+
* `gcp` the stack declares load-balancer-only and `staging-domain enable`
|
|
618
|
+
* (admin-domain.ts) opens it deliberately, so this never runs there.
|
|
619
|
+
*/
|
|
620
|
+
async function ensureGatewayIngress(
|
|
621
|
+
install: InstallTarget,
|
|
622
|
+
run: GcloudRunner,
|
|
623
|
+
): Promise<InvariantOutcome> {
|
|
624
|
+
const service = install.services.gateway;
|
|
625
|
+
const base = {
|
|
626
|
+
id: `ingress all on ${service}`,
|
|
627
|
+
onFailure: "abort" as const,
|
|
628
|
+
why: "the gcp-firebase stack's gateway ingress (#650); the health check, the staging domain and remote MCP reach its run.app URL",
|
|
629
|
+
};
|
|
630
|
+
const where = [`--region=${install.region}`, `--project=${install.projectId}`];
|
|
631
|
+
const describe = await run(["run", "services", "describe", service, ...where, "--format=json"]);
|
|
632
|
+
if (!describe.ok) return { ...base, status: "failed", error: describe.stderr.trim() };
|
|
633
|
+
const live = liveIngress(describe.stdout);
|
|
634
|
+
if (live === "all") return { ...base, status: "present" };
|
|
635
|
+
const update = await run([
|
|
636
|
+
"run",
|
|
637
|
+
"services",
|
|
638
|
+
"update",
|
|
639
|
+
service,
|
|
640
|
+
...where,
|
|
641
|
+
"--ingress=all",
|
|
642
|
+
"--quiet",
|
|
643
|
+
]);
|
|
644
|
+
return update.ok
|
|
645
|
+
? { ...base, id: `ingress ${live ?? "(unset)"} → all on ${service}`, status: "applied" }
|
|
646
|
+
: { ...base, status: "failed", error: update.stderr.trim() };
|
|
647
|
+
}
|
|
648
|
+
|
|
581
649
|
/**
|
|
582
650
|
* Idempotently ensure every IAM binding and CDN setting the install's stack
|
|
583
|
-
* declares (stack-contract.ts),
|
|
584
|
-
* on the default network are gone
|
|
651
|
+
* declares (stack-contract.ts), a gcp-firebase gateway's ingress, and that
|
|
652
|
+
* the firewall rules GCP pre-populates on the default network are gone
|
|
653
|
+
* (ABSENT_DEFAULT_FIREWALL_RULES). Bindings
|
|
585
654
|
* are only added, never removed; those firewall rules are the only deletion. Never
|
|
586
655
|
* throws for a gcloud failure; the report says what failed and whether the
|
|
587
656
|
* upgrade must stop.
|
|
@@ -622,7 +691,11 @@ export async function ensureStackInvariants(
|
|
|
622
691
|
? invariants
|
|
623
692
|
: invariants.filter((i) => i.member !== "iap-service-agent");
|
|
624
693
|
outcomes.push(...(await ensureIam(checkable, install, projectNumber, run, sleep)));
|
|
625
|
-
if (install.provider === "gcp")
|
|
694
|
+
if (install.provider === "gcp") {
|
|
695
|
+
outcomes.push(await ensureCdnPolicy(install, run));
|
|
696
|
+
outcomes.push(await ensureSvgDownloads(install, run));
|
|
697
|
+
}
|
|
698
|
+
if (install.provider === "gcp-firebase") outcomes.push(await ensureGatewayIngress(install, run));
|
|
626
699
|
outcomes.push(...(await removeDefaultFirewallRules(install.projectId, { run })));
|
|
627
700
|
|
|
628
701
|
return {
|
|
@@ -22,7 +22,7 @@ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitC
|
|
|
22
22
|
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
23
23
|
const SECRET = "s".repeat(40);
|
|
24
24
|
const ADMIN = "https://caelo-production-admin-abc-ew.a.run.app";
|
|
25
|
-
const target = { projectId: "proj", region: "europe-west1" };
|
|
25
|
+
const target = { provider: "gcp-firebase" as const, projectId: "proj", region: "europe-west1" };
|
|
26
26
|
|
|
27
27
|
function fakeGcloud(overrides: Record<string, GcloudResult> = {}) {
|
|
28
28
|
const answers: Record<string, GcloudResult> = {
|
package/src/staging-admin.ts
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
export const STAGING_PROVISION_SCOPE = "staging.provision";
|
|
21
21
|
const ENDPOINT = "/api/internal/staging/provision";
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** `staging-domain enable` is gcp-firebase only (its domain mapping is). */
|
|
24
|
+
export type StagingAdminTarget = Extract<AdminInternalTarget, { provider: "gcp-firebase" }>;
|
|
24
25
|
export type StagingAdminDeps = AdminInternalDeps;
|
|
25
26
|
|
|
26
27
|
/** Main's draft pages, as the admin reports them. */
|
package/src/state-backend.ts
CHANGED
|
@@ -127,8 +127,9 @@ export function localStateDir(installRoot: string): string {
|
|
|
127
127
|
* Pulumi reaches the bucket and the KMS key with the operator's Application
|
|
128
128
|
* Default Credentials. Both set `PULUMI_FALLBACK_TO_STATE_SECRETS_MANAGER`:
|
|
129
129
|
* Pulumi then takes the secrets provider (KMS key or passphrase salt) from
|
|
130
|
-
* the install's own state when the stack's
|
|
131
|
-
*
|
|
130
|
+
* the install's own state when the stack's settings file in its work dir
|
|
131
|
+
* (install-state.ts stackWorkDir) names none (wizards/gcp-pulumi.ts
|
|
132
|
+
* alignedStackSettings).
|
|
132
133
|
*
|
|
133
134
|
* @param meta the install (its `stateBackend`, absent = local)
|
|
134
135
|
* @param opts.installRoot `~/.caelo-<id>`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import type { GcloudResult } from "./gcloud.js";
|
|
5
|
+
import { adcNote } from "./state-command.js";
|
|
6
|
+
|
|
7
|
+
const result = (ok: boolean): GcloudResult => ({
|
|
8
|
+
ok,
|
|
9
|
+
stdout: ok ? "ya29.token" : "",
|
|
10
|
+
stderr: ok ? "" : "ERROR: Could not automatically determine credentials.",
|
|
11
|
+
exitCode: ok ? 0 : 1,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("adcNote (#816)", () => {
|
|
15
|
+
it("says nothing when this machine has working ADC", () => {
|
|
16
|
+
const calls: string[] = [];
|
|
17
|
+
expect(
|
|
18
|
+
adcNote((args) => {
|
|
19
|
+
calls.push(args.join(" "));
|
|
20
|
+
return result(true);
|
|
21
|
+
}),
|
|
22
|
+
).toBeNull();
|
|
23
|
+
expect(calls).toEqual(["auth application-default print-access-token"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("without ADC, says which commands need it and that upgrade does not", () => {
|
|
27
|
+
const note = adcNote(() => result(false));
|
|
28
|
+
expect(note).toContain("gcloud auth application-default login");
|
|
29
|
+
expect(note).toContain("upgrade, status and backup don't need them");
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/state-command.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { confirm, isCancel, log, note } from "@clack/prompts";
|
|
16
16
|
import { bold, red } from "kleur/colors";
|
|
17
|
-
import {
|
|
17
|
+
import { checkGcloudAuth } from "./gcloud.js";
|
|
18
18
|
import {
|
|
19
19
|
adoptStateBackend,
|
|
20
20
|
currentMetadata,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./install-state.js";
|
|
31
31
|
import { installIapAllowlist } from "./operator-access.js";
|
|
32
32
|
import { ensureOpsBackend } from "./ops-backend.js";
|
|
33
|
+
import { writePlanFile } from "./preview-plan.js";
|
|
33
34
|
import { provisionerServiceAccountEmail } from "./provisioner-identity.js";
|
|
34
35
|
import { selfCommand } from "./self-command.js";
|
|
35
36
|
import { attachInstall } from "./state-attach.js";
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
} from "./state-backend.js";
|
|
44
45
|
import { migrateState } from "./state-migrate.js";
|
|
45
46
|
import { realStatePulumiOps } from "./state-pulumi.js";
|
|
46
|
-
import { acquireStateLock, GcsStateStore, realSyncGcloud } from "./state-store.js";
|
|
47
|
+
import { acquireStateLock, GcsStateStore, realSyncGcloud, type SyncGcloud } from "./state-store.js";
|
|
47
48
|
import { clackReporter } from "./upgrade/reporter.js";
|
|
48
49
|
import { GCP_STACK_KNOB_DEFAULTS } from "./wizards/gcp-pulumi.js";
|
|
49
50
|
|
|
@@ -153,11 +154,12 @@ export async function stateCommand(argv: readonly string[]): Promise<void> {
|
|
|
153
154
|
);
|
|
154
155
|
process.exit(1);
|
|
155
156
|
}
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
158
|
-
log.error(red(
|
|
157
|
+
const auth = await checkGcloudAuth();
|
|
158
|
+
if (!auth.ok) {
|
|
159
|
+
log.error(red(`Nothing was changed: ${auth.error}`));
|
|
159
160
|
process.exit(1);
|
|
160
161
|
}
|
|
162
|
+
const operator = auth.account;
|
|
161
163
|
const provisioner = provisionerServiceAccountEmail(projectId);
|
|
162
164
|
note(
|
|
163
165
|
[
|
|
@@ -257,6 +259,7 @@ export async function stateCommand(argv: readonly string[]): Promise<void> {
|
|
|
257
259
|
renameSync(dir, moved);
|
|
258
260
|
return moved;
|
|
259
261
|
},
|
|
262
|
+
writePlan: (label, steps) => writePlanFile(root, label, steps),
|
|
260
263
|
},
|
|
261
264
|
);
|
|
262
265
|
if (!result.ok) {
|
|
@@ -272,6 +275,18 @@ export async function attachCommand(argv: readonly string[]): Promise<void> {
|
|
|
272
275
|
console.log("Usage: cms-provision attach --project <gcp-project-id>");
|
|
273
276
|
process.exit(2);
|
|
274
277
|
}
|
|
278
|
+
attachProject(projectId);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Attach the install whose records live in `projectId`'s ops bucket (or
|
|
283
|
+
* refresh this machine's copy of them); exits with the reason when that
|
|
284
|
+
* fails. `upgrade --project` runs it first, so the break-glass upgrade
|
|
285
|
+
* needs nothing to survive in $HOME between runs (#816).
|
|
286
|
+
*
|
|
287
|
+
* @returns the attached install's id
|
|
288
|
+
*/
|
|
289
|
+
export function attachProject(projectId: string): string {
|
|
275
290
|
const r = attachInstall(projectId, {
|
|
276
291
|
run: realSyncGcloud,
|
|
277
292
|
rootFor: installRoot,
|
|
@@ -287,7 +302,19 @@ export async function attachCommand(argv: readonly string[]): Promise<void> {
|
|
|
287
302
|
? `${r.domain} was already attached; its local copy of install.json is up to date.`
|
|
288
303
|
: `Attached ${bold(r.domain)} (${r.installId}). status, upgrade, backup and re-running the installer now work from this machine.`,
|
|
289
304
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
305
|
+
const adc = adcNote(realSyncGcloud);
|
|
306
|
+
if (adc) log.info(adc);
|
|
307
|
+
return r.installId;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The note `attach` prints about Application Default Credentials, or null
|
|
312
|
+
* when this machine has working ones. Only infrastructure runs (re-running
|
|
313
|
+
* the installer, `state migrate`) use ADC; `upgrade`, `status` and `backup`
|
|
314
|
+
* do not, so a note on every attach sent Owners on a detour (#816).
|
|
315
|
+
*/
|
|
316
|
+
export function adcNote(run: SyncGcloud): string | null {
|
|
317
|
+
const r = run(["auth", "application-default", "print-access-token"]);
|
|
318
|
+
if (r.ok) return null;
|
|
319
|
+
return "No Application Default Credentials on this machine. upgrade, status and backup don't need them; re-running the installer or `state migrate` does: gcloud auth application-default login";
|
|
293
320
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import type { PlannedStep } from "./preview-plan.js";
|
|
13
14
|
import {
|
|
14
15
|
gcsBackendRecord,
|
|
15
16
|
localStateNotice,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
type StatePulumiOps,
|
|
25
26
|
versionAtLeast,
|
|
26
27
|
} from "./state-migrate.js";
|
|
27
|
-
import { canonicalJson
|
|
28
|
+
import { canonicalJson } from "./state-pulumi.js";
|
|
28
29
|
import type { UpgradeReporter } from "./upgrade/reporter.js";
|
|
29
30
|
|
|
30
31
|
interface FakeStack {
|
|
@@ -32,6 +33,20 @@ interface FakeStack {
|
|
|
32
33
|
secretsProvider: string;
|
|
33
34
|
pending: number;
|
|
34
35
|
drift: number;
|
|
36
|
+
/** What the preview plans; `drift` in-place updates when unset. */
|
|
37
|
+
plan?: PlannedStep[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `n` in-place updates of IAM members, the shape most drift has. */
|
|
41
|
+
function updates(n: number): PlannedStep[] {
|
|
42
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
43
|
+
op: "update" as const,
|
|
44
|
+
type: "gcp:projects/iAMMember:IAMMember",
|
|
45
|
+
name: `binding-${i}`,
|
|
46
|
+
urn: `urn:pulumi:production::caelo-gcp-firebase::gcp:projects/iAMMember:IAMMember::binding-${i}`,
|
|
47
|
+
diffs: ["member"],
|
|
48
|
+
replaceReasons: [],
|
|
49
|
+
}));
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
const ROOT = "/home/op/.caelo-gcp-firebase-acme";
|
|
@@ -60,7 +75,7 @@ function fakePulumi(version = "3.260.0") {
|
|
|
60
75
|
async previewChanges(b) {
|
|
61
76
|
const s = stacks.get(b.url);
|
|
62
77
|
if (!s) throw new Error(`no stack in ${b.url}`);
|
|
63
|
-
return s.drift;
|
|
78
|
+
return s.plan ?? updates(s.drift);
|
|
64
79
|
},
|
|
65
80
|
async locked(b) {
|
|
66
81
|
return faults.lockedUrl === b.url;
|
|
@@ -115,6 +130,7 @@ function recorder(): UpgradeReporter & { lines: string[] } {
|
|
|
115
130
|
done: (m) => lines.push(`done ${m}`),
|
|
116
131
|
failed: (m) => lines.push(`failed ${m}`),
|
|
117
132
|
warned: (m) => lines.push(`warned ${m}`),
|
|
133
|
+
progress: (m) => lines.push(`progress ${m}`),
|
|
118
134
|
};
|
|
119
135
|
},
|
|
120
136
|
info: (m) => lines.push(`info ${m}`),
|
|
@@ -137,6 +153,7 @@ function machine(initial: StateBackendRecord = { kind: "local" }) {
|
|
|
137
153
|
locked: false,
|
|
138
154
|
acceptCost: true,
|
|
139
155
|
bucketRecord: (initial.kind === "gcs" ? "gcs" : null) as "gcs" | "local" | null,
|
|
156
|
+
planFiles: [] as { label: string; steps: number }[],
|
|
140
157
|
};
|
|
141
158
|
const meta = (): MigrateInstall => ({
|
|
142
159
|
installId: "gcp-firebase-acme",
|
|
@@ -188,6 +205,10 @@ function machine(initial: StateBackendRecord = { kind: "local" }) {
|
|
|
188
205
|
m.retired.push(`${ROOT}/state.migrated-x`);
|
|
189
206
|
return `${ROOT}/state.migrated-x`;
|
|
190
207
|
},
|
|
208
|
+
writePlan: (label, steps) => {
|
|
209
|
+
m.planFiles.push({ label, steps: steps.length });
|
|
210
|
+
return `${ROOT}/plan-${label}.txt`;
|
|
211
|
+
},
|
|
191
212
|
};
|
|
192
213
|
p.stacks.set(localUrl, {
|
|
193
214
|
outputs: {
|
|
@@ -302,6 +323,8 @@ describe("migrateState — failures leave the source authoritative", () => {
|
|
|
302
323
|
t.p.faults.driftOnTarget = 2;
|
|
303
324
|
const r = await migrateState(t.meta(), { to: "remote", dryRun: false }, t.deps);
|
|
304
325
|
expect(r.ok).toBe(false);
|
|
326
|
+
expect(t.report.lines.join("\n")).toContain("The copy's preview plans:");
|
|
327
|
+
expect(t.m.planFiles).toEqual([{ label: "state-migrate-copy", steps: 2 }]);
|
|
305
328
|
expect(t.m.backend).toEqual({ kind: "local" });
|
|
306
329
|
expect(t.p.stacks.has(t.gcsUrl)).toBe(false);
|
|
307
330
|
});
|
|
@@ -327,6 +350,36 @@ describe("migrateState — failures leave the source authoritative", () => {
|
|
|
327
350
|
expect(t.m.ensureRemoteCalls).toEqual([]);
|
|
328
351
|
});
|
|
329
352
|
|
|
353
|
+
it("the refusal lists every planned step, replaces and data loss first, and writes the diff (#737)", async () => {
|
|
354
|
+
const t = machine();
|
|
355
|
+
const local = t.p.stacks.get(t.localUrl);
|
|
356
|
+
if (local) {
|
|
357
|
+
local.plan = [
|
|
358
|
+
...updates(1),
|
|
359
|
+
{
|
|
360
|
+
op: "replace",
|
|
361
|
+
type: "gcp:sql/databaseInstance:DatabaseInstance",
|
|
362
|
+
name: "caelo-production-pg",
|
|
363
|
+
urn: "urn:pulumi:production::caelo-gcp-firebase::gcp:sql/databaseInstance:DatabaseInstance::caelo-production-pg",
|
|
364
|
+
diffs: ["region", "settings"],
|
|
365
|
+
replaceReasons: ["region"],
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
const r = await migrateState(t.meta(), { to: "remote", dryRun: false }, t.deps);
|
|
370
|
+
expect(r.ok).toBe(false);
|
|
371
|
+
if (r.ok) return;
|
|
372
|
+
expect(r.error).toContain("a preview shows 2 change(s)");
|
|
373
|
+
expect(r.error).toContain(
|
|
374
|
+
"gcp:sql/databaseInstance:DatabaseInstance caelo-production-pg (replaced because region changes) <-- LOSES DATA: Cloud SQL instance",
|
|
375
|
+
);
|
|
376
|
+
expect(r.error).toContain("gcp:projects/iAMMember:IAMMember binding-0 (member)");
|
|
377
|
+
expect(r.error.indexOf("REPLACES")).toBeLessThan(r.error.indexOf("Updates in place"));
|
|
378
|
+
expect(r.error).toContain(`Full diff: ${ROOT}/plan-state-migrate.txt`);
|
|
379
|
+
expect(t.m.planFiles).toEqual([{ label: "state-migrate", steps: 2 }]);
|
|
380
|
+
expect(t.m.backups).toBe(0);
|
|
381
|
+
});
|
|
382
|
+
|
|
330
383
|
it("refuses a stack with pending operations", async () => {
|
|
331
384
|
const t = machine();
|
|
332
385
|
const local = t.p.stacks.get(t.localUrl);
|
|
@@ -448,6 +501,9 @@ describe("migrateState — dry run", () => {
|
|
|
448
501
|
const out = t.report.lines.join("\n");
|
|
449
502
|
expect(out).toContain("Dry run — nothing changed");
|
|
450
503
|
expect(out).toContain("would create gs://acme-caelo-ops");
|
|
504
|
+
// #737 — the preview's list even when it would migrate.
|
|
505
|
+
expect(out).toContain("The preview plans no changes.");
|
|
506
|
+
expect(t.m.planFiles).toEqual([{ label: "state-migrate-dry-run", steps: 0 }]);
|
|
451
507
|
});
|
|
452
508
|
});
|
|
453
509
|
|
|
@@ -465,11 +521,6 @@ describe("helpers", () => {
|
|
|
465
521
|
);
|
|
466
522
|
});
|
|
467
523
|
|
|
468
|
-
it("changeCount ignores unchanged resources", () => {
|
|
469
|
-
expect(changeCount({ same: 120 })).toBe(0);
|
|
470
|
-
expect(changeCount({ same: 100, update: 2, create: 1 })).toBe(3);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
524
|
it("upgrade's notice points a local GCP install at state migrate, dry run first", () => {
|
|
474
525
|
const n = localStateNotice({ installId: "gcp-firebase-viu-one", provider: "gcp-firebase" });
|
|
475
526
|
expect(n).toContain("state migrate --install gcp-firebase-viu-one --dry-run");
|
package/src/state-migrate.ts
CHANGED
|
@@ -24,13 +24,19 @@
|
|
|
24
24
|
* 6. flip — install.json records the new backend (locally and in the
|
|
25
25
|
* bucket); the old local state dir is renamed, never deleted.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Every preview's planned changes are listed per resource (#737): grouped
|
|
28
|
+
* creates / updates / REPLACES / DELETES, data-bearing resources named, the
|
|
29
|
+
* full diff written to a file in the install dir.
|
|
30
|
+
*
|
|
31
|
+
* `--dry-run` runs the read-only preflight and reports what it would do,
|
|
32
|
+
* including the preview's list.
|
|
28
33
|
* The engine takes its side effects as {@link MigrateDeps} so the whole
|
|
29
34
|
* sequence is tested against fakes (state-migrate.test.ts); state-command.ts
|
|
30
35
|
* wires the real Pulumi CLI, gcloud and file system.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
import type { OpsBackendResult } from "./ops-backend.js";
|
|
39
|
+
import { formatPlan, type PlannedStep } from "./preview-plan.js";
|
|
34
40
|
import { selfCommand } from "./self-command.js";
|
|
35
41
|
import {
|
|
36
42
|
describeBackend,
|
|
@@ -56,8 +62,8 @@ export interface StatePulumiOps {
|
|
|
56
62
|
stackExists(backend: ResolvedBackend): Promise<boolean>;
|
|
57
63
|
/** Pending operations recorded in the stack's state (an interrupted run). */
|
|
58
64
|
pendingOperations(backend: ResolvedBackend): Promise<number>;
|
|
59
|
-
/**
|
|
60
|
-
previewChanges(backend: ResolvedBackend): Promise<
|
|
65
|
+
/** The changes a preview plans, per resource (none = converged). */
|
|
66
|
+
previewChanges(backend: ResolvedBackend): Promise<readonly PlannedStep[]>;
|
|
61
67
|
/** sha256 of the stack outputs, secrets included. Never the values. */
|
|
62
68
|
outputsDigest(backend: ResolvedBackend): Promise<string>;
|
|
63
69
|
/** Whether a Pulumi operation holds the stack's lock in `backend` right now. */
|
|
@@ -106,6 +112,11 @@ export interface MigrateDeps {
|
|
|
106
112
|
lock(): Promise<() => Promise<void>>;
|
|
107
113
|
/** Rename the local state dir out of the way; returns where it went. */
|
|
108
114
|
retireLocalState(): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Write a preview's full plan to a file in the install dir (preview-plan.ts
|
|
117
|
+
* writePlanFile); returns its path.
|
|
118
|
+
*/
|
|
119
|
+
writePlan(label: string, steps: readonly PlannedStep[]): string;
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
export interface MigrateOptions {
|
|
@@ -141,6 +152,21 @@ export function versionAtLeast(a: string, b: string): boolean {
|
|
|
141
152
|
|
|
142
153
|
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
|
|
143
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The plan's grouped list plus where its full diff was written, for a
|
|
157
|
+
* refusal or a dry run. A plan file that cannot be written is said, not
|
|
158
|
+
* fatal: the list is on screen anyway.
|
|
159
|
+
*/
|
|
160
|
+
function planReport(deps: MigrateDeps, label: string, steps: readonly PlannedStep[]): string {
|
|
161
|
+
let where: string;
|
|
162
|
+
try {
|
|
163
|
+
where = `Full diff: ${deps.writePlan(label, steps)}`;
|
|
164
|
+
} catch (e) {
|
|
165
|
+
where = `(The full diff could not be written: ${errorText(e)})`;
|
|
166
|
+
}
|
|
167
|
+
return [...formatPlan(steps).map((l) => ` ${l}`), where].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
/**
|
|
145
171
|
* Run the migration. Never throws for an expected failure: the result says
|
|
146
172
|
* what failed, and at every failure point the source backend is still the
|
|
@@ -198,6 +224,7 @@ export async function migrateState(
|
|
|
198
224
|
let sourceDigest: string;
|
|
199
225
|
/** The source's checkpoint, to notice another run writing it meanwhile. */
|
|
200
226
|
let sourceState: string;
|
|
227
|
+
let sourcePlan: readonly PlannedStep[];
|
|
201
228
|
try {
|
|
202
229
|
const version = await pulumi.cliVersion();
|
|
203
230
|
if (!version) {
|
|
@@ -232,12 +259,12 @@ export async function migrateState(
|
|
|
232
259
|
error: `The stack records ${pending} pending operation(s) from an interrupted Pulumi run. Run upgrade first, then re-run the installer (${selfCommand()}, pick this install) so the stack is refreshed, and migrate after that. Nothing was changed.`,
|
|
233
260
|
};
|
|
234
261
|
}
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
pre.failed(`A preview shows ${
|
|
262
|
+
sourcePlan = await pulumi.previewChanges(from);
|
|
263
|
+
if (sourcePlan.length > 0) {
|
|
264
|
+
pre.failed(`A preview shows ${sourcePlan.length} change(s)`);
|
|
238
265
|
return {
|
|
239
266
|
ok: false,
|
|
240
|
-
error: `The stack is not converged (a preview shows ${
|
|
267
|
+
error: `The stack is not converged (a preview shows ${sourcePlan.length} change(s)), so a copy can't be proven identical. Nothing was changed. The planned changes:\n${planReport(deps, "state-migrate", sourcePlan)}\nRun upgrade first, then re-run the installer (${selfCommand()}, pick this install): it shows this list again and asks before it replaces or deletes anything. Migrate once it reports no changes.`,
|
|
241
268
|
};
|
|
242
269
|
}
|
|
243
270
|
sourceDigest = await pulumi.outputsDigest(from);
|
|
@@ -274,7 +301,12 @@ export async function migrateState(
|
|
|
274
301
|
: []),
|
|
275
302
|
);
|
|
276
303
|
report.info(
|
|
277
|
-
[
|
|
304
|
+
[
|
|
305
|
+
"Dry run — nothing changed. The migration would:",
|
|
306
|
+
...plan.map((p) => ` ${p}`),
|
|
307
|
+
"The source stack's preview:",
|
|
308
|
+
planReport(deps, "state-migrate-dry-run", sourcePlan),
|
|
309
|
+
].join("\n"),
|
|
278
310
|
);
|
|
279
311
|
return { ok: true, changed: false };
|
|
280
312
|
}
|
|
@@ -385,8 +417,14 @@ export async function migrateState(
|
|
|
385
417
|
}
|
|
386
418
|
try {
|
|
387
419
|
const changes = await pulumi.previewChanges(to);
|
|
388
|
-
if (changes > 0)
|
|
389
|
-
|
|
420
|
+
if (changes.length > 0) {
|
|
421
|
+
report.info(
|
|
422
|
+
`The copy's preview plans:\n${planReport(deps, "state-migrate-copy", changes)}`,
|
|
423
|
+
);
|
|
424
|
+
return abandon(
|
|
425
|
+
`Verification failed: a preview of the copy shows ${changes.length} change(s) (listed above)`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
390
428
|
if ((await pulumi.outputsDigest(to)) !== sourceDigest) {
|
|
391
429
|
return abandon("Verification failed: the copy's outputs differ from the source's");
|
|
392
430
|
}
|