@caelo-cms/provisioning 0.11.9 → 0.11.11
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/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +12 -3
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +143 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +23 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +69 -10
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- 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 +68 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +93 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +295 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +43 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +148 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/converge-db-trust.d.ts +44 -0
- package/dist/converge-db-trust.d.ts.map +1 -0
- package/dist/converge-db-trust.js +158 -0
- package/dist/converge-db-trust.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +5 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +11 -13
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +8 -0
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job.d.ts +17 -0
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +12 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +34 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +45 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.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 +45 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +4 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +4 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +13 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +20 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +8 -3
- package/dist/migration-runner.js.map +1 -1
- package/dist/migration-targets.d.ts +11 -0
- package/dist/migration-targets.d.ts.map +1 -0
- package/dist/migration-targets.js +12 -0
- package/dist/migration-targets.js.map +1 -0
- package/dist/operator-access.d.ts +10 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +5 -0
- package/dist/operator-access.js.map +1 -1
- package/dist/pool-budget-stack.d.ts +13 -1
- package/dist/pool-budget-stack.d.ts.map +1 -1
- package/dist/pool-budget-stack.js +11 -1
- package/dist/pool-budget-stack.js.map +1 -1
- package/dist/public-role-isolation.d.ts +7 -3
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +25 -13
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +11 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +40 -25
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +75 -0
- package/dist/scheduler-setup.d.ts.map +1 -0
- package/dist/scheduler-setup.js +331 -0
- package/dist/scheduler-setup.js.map +1 -0
- package/dist/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.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 +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +43 -2
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +87 -1
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-scheduler-interval.d.ts +21 -0
- package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
- package/dist/self-hosted-scheduler-interval.js +68 -0
- package/dist/self-hosted-scheduler-interval.js.map +1 -0
- package/dist/self-hosted-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +19 -7
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +78 -25
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +234 -28
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +397 -39
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-database.d.ts +22 -0
- package/dist/staging-database.d.ts.map +1 -0
- package/dist/staging-database.js +117 -0
- package/dist/staging-database.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +207 -20
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +19 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +37 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +13 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +50 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -0
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +31 -0
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +103 -18
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +129 -27
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +2 -1
- package/src/adopt-existing.test.ts +85 -0
- package/src/adopt-existing.ts +141 -3
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +39 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +4 -1
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +71 -5
- package/src/cloudsql-tls.test.ts +255 -0
- package/src/cloudsql-tls.ts +359 -0
- package/src/compose.ts +193 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +11 -12
- package/src/converge-job-preflight.test.ts +5 -0
- package/src/converge-job-preflight.ts +8 -0
- package/src/converge-job.test.ts +70 -0
- package/src/converge-job.ts +32 -0
- package/src/database-tls-builders.test.ts +56 -0
- package/src/deployed-release.test.ts +107 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +50 -12
- package/src/gcp-names.ts +56 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +10 -4
- package/src/index.ts +7 -0
- package/src/infra-contract.test.ts +7 -3
- package/src/infra-contract.ts +45 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +33 -4
- package/src/lifecycle.ts +23 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +20 -4
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +39 -14
- package/src/public-role-isolation.test.ts +18 -8
- package/src/public-role-isolation.ts +31 -17
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +37 -14
- package/src/runtime-secrets.ts +87 -27
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +180 -6
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +87 -3
- package/src/self-hosted-roles.ts +101 -3
- package/src/self-hosted-scheduler-interval.test.ts +66 -0
- package/src/self-hosted-scheduler-interval.ts +79 -0
- package/src/self-hosted-start.test.ts +35 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +104 -14
- package/src/self-hosted-upgrade.ts +123 -31
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +199 -31
- package/src/stack-contract.ts +472 -45
- package/src/stack-converge-secrets.test.ts +28 -13
- package/src/stack-converge.test.ts +203 -28
- package/src/stack-converge.ts +92 -30
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +249 -19
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +438 -46
- package/src/upgrade/upgrade.ts +64 -6
- package/src/upgrader-gcp.ts +66 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +103 -0
- package/src/wizards/gcp-cost.ts +125 -18
- package/src/wizards/gcp.ts +164 -22
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +72 -14
- package/stacks/azure/index.ts +108 -21
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +133 -24
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +212 -34
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +6 -1
- package/stacks/self-hosted/index.ts +40 -5
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1373 — where the converge job (#819) opens its database queue, and whom
|
|
5
|
+
* it trusts there, BEFORE the install's database TLS step (#1338) ran.
|
|
6
|
+
*
|
|
7
|
+
* The job writes the update timeline and the request's end through that
|
|
8
|
+
* queue. Its code connects with verify-full only, which needs the
|
|
9
|
+
* instance's DNS name, and on an install whose services still dial the
|
|
10
|
+
* instance's IP the DNS name exists only once the TLS step has run. When
|
|
11
|
+
* the job ran that step before opening the queue, a failure of the step
|
|
12
|
+
* reached only the job's log: Settings → Updates showed "Interrupted" and
|
|
13
|
+
* nothing else. So the job first opens the queue on the host the install
|
|
14
|
+
* dials today and runs the TLS step as a timeline step of its own
|
|
15
|
+
* (converge-job.ts `secureDatabase`).
|
|
16
|
+
*
|
|
17
|
+
* Dialling an IP, the host-name half of verify-full cannot hold (the
|
|
18
|
+
* certificate names no IP). The connection must still authenticate the
|
|
19
|
+
* server: the job's password is the provisioner's access token, worth far
|
|
20
|
+
* more than a database password, so `sslmode=require` (encrypted, server
|
|
21
|
+
* unchecked) is not an option. The trust comes from the Cloud SQL Admin
|
|
22
|
+
* API, over an authenticated channel, about this one instance:
|
|
23
|
+
*
|
|
24
|
+
* - per-instance CA (`GOOGLE_MANAGED_INTERNAL_CA`, the default before
|
|
25
|
+
* #1338): the instance's own server CA (`serverCaCert`). Nobody else
|
|
26
|
+
* holds a certificate from it, so a verified chain is this instance.
|
|
27
|
+
* - shared CA (`GOOGLE_MANAGED_CAS_CA`): always pinned, because the
|
|
28
|
+
* shared CA signs other customers' instances too. With its certificate
|
|
29
|
+
* active: Google's shared-CA bundle (the one every image ships) and the
|
|
30
|
+
* fingerprints `listServerCertificates` names; not rotated in yet: the
|
|
31
|
+
* one certificate the API names active. A listing that does not name
|
|
32
|
+
* the active certificate with a well-formed fingerprint is refused —
|
|
33
|
+
* fail closed, never a guess.
|
|
34
|
+
* - any other CA mode: refused.
|
|
35
|
+
*
|
|
36
|
+
* No input but the metadata server's placement and the APIs: the job's
|
|
37
|
+
* scrubbed-environment contract (§11.C) is unchanged.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
import { CLOUD_SQL_CA_BUNDLE, isIpLiteral } from "@caelo-cms/shared";
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
import { describeInstance } from "./cloudsql-tls.js";
|
|
44
|
+
import { findSqlInstance } from "./gateway-credentials.js";
|
|
45
|
+
import { gcloud } from "./gcloud.js";
|
|
46
|
+
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
47
|
+
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
48
|
+
import type { HttpFetch } from "./runtime-secrets.js";
|
|
49
|
+
import { CLOUD_SQL_SERVER_CA_MODE } from "./stack-contract.js";
|
|
50
|
+
import { type UpgraderPlacement, upgraderDatabaseHost } from "./upgrader-gcp.js";
|
|
51
|
+
|
|
52
|
+
/** How the converge job reaches the database. */
|
|
53
|
+
export type ConvergeDatabaseTarget =
|
|
54
|
+
/** The services dial the DNS name already: verify-full, nothing to set up. */
|
|
55
|
+
| { readonly kind: "dns"; readonly host: string }
|
|
56
|
+
/**
|
|
57
|
+
* The services dial the instance's IP: connect there, verify the chain
|
|
58
|
+
* against `caPem` and, when `pinnedSha1` is set, the certificate's
|
|
59
|
+
* fingerprint (lower-case hex, no colons) against it.
|
|
60
|
+
*/
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: "ip";
|
|
63
|
+
readonly host: string;
|
|
64
|
+
readonly caPem: string;
|
|
65
|
+
readonly pinnedSha1: readonly string[] | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface ConvergeDatabaseTargetDeps {
|
|
69
|
+
readonly run?: GcloudRunner;
|
|
70
|
+
readonly http?: HttpFetch;
|
|
71
|
+
/** The shipped shared-CA bundle (tests pass their own). */
|
|
72
|
+
readonly readSharedCaBundle?: () => string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A SHA-1 fingerprint as hex without separators, lower case (`AB:CD…` and `abcd…` compare equal). */
|
|
76
|
+
export function normalizedSha1(fingerprint: string): string {
|
|
77
|
+
return fingerprint.replace(/[^0-9a-f]/gi, "").toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The host the install dials today and, when it is the instance's IP, the
|
|
82
|
+
* server identity the converge job's connection must verify there. Throws
|
|
83
|
+
* with what could not be read: without it the job must not connect.
|
|
84
|
+
*/
|
|
85
|
+
export async function convergeDatabaseTarget(
|
|
86
|
+
placement: Pick<UpgraderPlacement, "projectId" | "region">,
|
|
87
|
+
deps: ConvergeDatabaseTargetDeps = {},
|
|
88
|
+
): Promise<ConvergeDatabaseTarget> {
|
|
89
|
+
const run = deps.run ?? gcloud;
|
|
90
|
+
const host = await upgraderDatabaseHost(placement, run);
|
|
91
|
+
if (!isIpLiteral(host)) return { kind: "dns", host };
|
|
92
|
+
const install = { projectId: placement.projectId, env: GCP_STACK_ENV };
|
|
93
|
+
const lookup = await findSqlInstance(install, { run });
|
|
94
|
+
if (lookup.status !== "found") {
|
|
95
|
+
throw new Error(`could not find the install's Cloud SQL instance (${lookup.status})`);
|
|
96
|
+
}
|
|
97
|
+
const target = { ...install, sqlInstance: lookup.name };
|
|
98
|
+
const described = await describeInstance(target, run);
|
|
99
|
+
if (!described.ok) throw new Error(described.error);
|
|
100
|
+
const d = described.d;
|
|
101
|
+
const mode = d.settings?.ipConfiguration?.serverCaMode;
|
|
102
|
+
const ownCa = d.serverCaCert?.cert?.trim() || null;
|
|
103
|
+
if (mode === CLOUD_SQL_SERVER_CA_MODE) {
|
|
104
|
+
const listing = await serverCertificateListing(
|
|
105
|
+
target,
|
|
106
|
+
run,
|
|
107
|
+
deps.http ?? (fetch as unknown as HttpFetch),
|
|
108
|
+
);
|
|
109
|
+
// Fail closed: once the shared CA may be active, every connection is
|
|
110
|
+
// pinned to a certificate the API names — the shared CA alone would
|
|
111
|
+
// also vouch for other customers' instances.
|
|
112
|
+
const sharedBundle = (
|
|
113
|
+
deps.readSharedCaBundle ?? (() => readFileSync(CLOUD_SQL_CA_BUNDLE, "utf8"))
|
|
114
|
+
)();
|
|
115
|
+
if (listing.fingerprints.includes(listing.active)) {
|
|
116
|
+
return { kind: "ip", host, caPem: sharedBundle, pinnedSha1: listing.fingerprints };
|
|
117
|
+
}
|
|
118
|
+
// Moved to the shared CA, its certificate not rotated in yet: the
|
|
119
|
+
// server still presents the active one from its own CA. Pinned to
|
|
120
|
+
// exactly that certificate, so which CA vouches for it does not widen
|
|
121
|
+
// what is accepted.
|
|
122
|
+
const caPem = [ownCa, sharedBundle].filter((c): c is string => Boolean(c)).join("\n");
|
|
123
|
+
return { kind: "ip", host, caPem, pinnedSha1: [listing.active] };
|
|
124
|
+
}
|
|
125
|
+
// Per-instance CA (the default, which older instances do not name):
|
|
126
|
+
// nobody else holds a certificate from it, so the verified chain is this
|
|
127
|
+
// instance. Any other mode (a customer-managed CA) is not one this job
|
|
128
|
+
// can reason about.
|
|
129
|
+
if (mode !== undefined && mode !== PER_INSTANCE_CA_MODE) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`${lookup.name} uses the server CA mode ${mode}, which the converge job cannot verify a connection to ${host} for`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!ownCa) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${lookup.name} names no server CA certificate (serverCaCert), so the connection to ${host} could not be verified`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return { kind: "ip", host, caPem: ownCa, pinnedSha1: null };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Cloud SQL's default per-instance server CA mode. */
|
|
143
|
+
const PER_INSTANCE_CA_MODE = "GOOGLE_MANAGED_INTERNAL_CA";
|
|
144
|
+
|
|
145
|
+
const SHA1_HEX = /^[0-9a-f]{40}$/;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The Admin API's `listServerCertificates` answer, validated: the active
|
|
149
|
+
* certificate's fingerprint and every listed one, normalized. Throws on
|
|
150
|
+
* anything else — a missing `activeVersion`, a certificate without a
|
|
151
|
+
* well-formed fingerprint — since a guessed listing would be a guessed
|
|
152
|
+
* trust decision.
|
|
153
|
+
*/
|
|
154
|
+
async function serverCertificateListing(
|
|
155
|
+
t: { readonly projectId: string; readonly sqlInstance: string },
|
|
156
|
+
run: GcloudRunner,
|
|
157
|
+
http: HttpFetch,
|
|
158
|
+
): Promise<{ readonly active: string; readonly fingerprints: readonly string[] }> {
|
|
159
|
+
const token = await run(["auth", "print-access-token"]);
|
|
160
|
+
const bearer = token.ok ? token.stdout.trim() : "";
|
|
161
|
+
if (!bearer) throw new Error(`gcloud auth print-access-token: ${token.stderr.trim()}`);
|
|
162
|
+
const res = await http(
|
|
163
|
+
`https://sqladmin.googleapis.com/v1/projects/${encodeURIComponent(t.projectId)}/instances/${encodeURIComponent(t.sqlInstance)}/listServerCertificates`,
|
|
164
|
+
{ method: "GET", headers: { Authorization: `Bearer ${bearer}` } },
|
|
165
|
+
);
|
|
166
|
+
const text = await res.text();
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw new Error(`list server certificates: HTTP ${res.status} ${text.slice(0, 300)}`);
|
|
169
|
+
}
|
|
170
|
+
let body: unknown;
|
|
171
|
+
try {
|
|
172
|
+
body = JSON.parse(text);
|
|
173
|
+
} catch {
|
|
174
|
+
throw new Error("list server certificates: the Cloud SQL Admin API answered no JSON");
|
|
175
|
+
}
|
|
176
|
+
const parsed = ListingSchema.safeParse(body);
|
|
177
|
+
if (!parsed.success) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`list server certificates: the answer does not name the active certificate and the listed ones with SHA-1 fingerprints (${parsed.error.issues[0]?.message ?? "invalid"}), so the connection is not trusted`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
active: parsed.data.activeVersion,
|
|
184
|
+
fingerprints: parsed.data.serverCerts.map((c) => c.sha1Fingerprint),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const Sha1 = z
|
|
189
|
+
.string()
|
|
190
|
+
.transform(normalizedSha1)
|
|
191
|
+
.refine((f) => SHA1_HEX.test(f), "a SHA-1 fingerprint");
|
|
192
|
+
|
|
193
|
+
const ListingSchema = z.object({
|
|
194
|
+
activeVersion: Sha1,
|
|
195
|
+
// Before the first shared-CA certificate exists the API may list none.
|
|
196
|
+
serverCerts: z.array(z.object({ sha1Fingerprint: Sha1 })).default([]),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Why a server certificate with `fingerprint` (as `node:tls` prints it) is
|
|
201
|
+
* not the instance `target` names, or null when it is. Only for an `ip`
|
|
202
|
+
* target; the chain itself is checked against `caPem` by the TLS layer.
|
|
203
|
+
*/
|
|
204
|
+
export function serverIdentityProblem(
|
|
205
|
+
target: Extract<ConvergeDatabaseTarget, { kind: "ip" }>,
|
|
206
|
+
fingerprint: string,
|
|
207
|
+
): string | null {
|
|
208
|
+
if (target.pinnedSha1 === null) return null;
|
|
209
|
+
return target.pinnedSha1.includes(normalizedSha1(fingerprint))
|
|
210
|
+
? null
|
|
211
|
+
: `the database at ${target.host} presented a certificate (SHA-1 ${normalizedSha1(fingerprint)}) that the Cloud SQL Admin API does not list for this install's instance`;
|
|
212
|
+
}
|
package/src/converge-job-gcp.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "./install-state.js";
|
|
43
43
|
import { runMigrationsViaCloudRunJob } from "./migration-runner.js";
|
|
44
44
|
import { installIapAllowlist } from "./operator-access.js";
|
|
45
|
-
import { fetchVerifiedReleaseManifest } from "./release-manifest.js";
|
|
45
|
+
import { fetchVerifiedReleaseManifest, manifestServiceImages } from "./release-manifest.js";
|
|
46
46
|
import type { HttpFetch } from "./runtime-secrets.js";
|
|
47
47
|
import { publicSiteUrl, STAGING_HOST_ENV } from "./stack-contract.js";
|
|
48
48
|
import { attachInstall } from "./state-attach.js";
|
|
@@ -203,10 +203,14 @@ export function startedExecutionName(stdout: string, stderr: string): string | n
|
|
|
203
203
|
return /^[a-z0-9-]+$/.test(name) ? name : null;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Every dependency of {@link runConvergeJob} except the database (the queue
|
|
208
|
+
* and how it is secured, #1373) and the log.
|
|
209
|
+
*/
|
|
207
210
|
export function gcpConvergeDeps(
|
|
208
211
|
placement: UpgraderPlacement,
|
|
209
212
|
queue: ConvergeQueue,
|
|
213
|
+
secureDatabase: ConvergeJobDeps["secureDatabase"],
|
|
210
214
|
log: JobLog,
|
|
211
215
|
): ConvergeJobDeps {
|
|
212
216
|
const run: GcloudRunner = gcloud;
|
|
@@ -300,6 +304,9 @@ export function gcpConvergeDeps(
|
|
|
300
304
|
domain: meta.domain,
|
|
301
305
|
stagingHost: installStagingHost(meta),
|
|
302
306
|
ownerEmail: meta.ownerEmail,
|
|
307
|
+
...(meta.schedulerIntervalMinutes !== undefined
|
|
308
|
+
? { schedulerIntervalMinutes: meta.schedulerIntervalMinutes }
|
|
309
|
+
: {}),
|
|
303
310
|
targetTag: m.version,
|
|
304
311
|
// This code is the target release's (stage 2), so its contract is it.
|
|
305
312
|
infraContractVersion: INFRA_CONTRACT_VERSION,
|
|
@@ -314,16 +321,7 @@ export function gcpConvergeDeps(
|
|
|
314
321
|
sleep: realSleep,
|
|
315
322
|
reporter,
|
|
316
323
|
// The signed manifest's digests — never a registry tag lookup.
|
|
317
|
-
resolveImages: async () => (
|
|
318
|
-
ok: true,
|
|
319
|
-
images: {
|
|
320
|
-
admin: { digest: m.digests.admin, imageRef: releaseImageRef("admin", m.digests.admin) },
|
|
321
|
-
gateway: {
|
|
322
|
-
digest: m.digests.gateway,
|
|
323
|
-
imageRef: releaseImageRef("gateway", m.digests.gateway),
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
}),
|
|
324
|
+
resolveImages: async () => manifestServiceImages(m),
|
|
327
325
|
verifyImages: verifyCosignAll,
|
|
328
326
|
migrate: (imageRef) =>
|
|
329
327
|
runMigrationsViaCloudRunJob(
|
|
@@ -364,6 +362,7 @@ export function gcpConvergeDeps(
|
|
|
364
362
|
|
|
365
363
|
return {
|
|
366
364
|
queue,
|
|
365
|
+
secureDatabase,
|
|
367
366
|
projectId,
|
|
368
367
|
region,
|
|
369
368
|
iam: gcloudIamTester(run, http),
|
|
@@ -43,6 +43,11 @@ const PREDEFINED: Record<string, readonly string[]> = {
|
|
|
43
43
|
"cloudscheduler.jobs.get",
|
|
44
44
|
"cloudscheduler.jobs.update",
|
|
45
45
|
],
|
|
46
|
+
"roles/workflows.editor": [
|
|
47
|
+
"workflows.workflows.create",
|
|
48
|
+
"workflows.workflows.get",
|
|
49
|
+
"workflows.workflows.update",
|
|
50
|
+
],
|
|
46
51
|
"roles/logging.logWriter": ["logging.logEntries.create"],
|
|
47
52
|
};
|
|
48
53
|
|
|
@@ -62,6 +62,14 @@ export const INSTALL_ROLE_USES: Record<
|
|
|
62
62
|
"cloudscheduler.jobs.update",
|
|
63
63
|
],
|
|
64
64
|
},
|
|
65
|
+
"roles/workflows.editor": {
|
|
66
|
+
need: "deploy and update the workflow the scheduler tick runs (#1140)",
|
|
67
|
+
permissions: [
|
|
68
|
+
"workflows.workflows.create",
|
|
69
|
+
"workflows.workflows.get",
|
|
70
|
+
"workflows.workflows.update",
|
|
71
|
+
],
|
|
72
|
+
},
|
|
65
73
|
"roles/logging.logWriter": {
|
|
66
74
|
need: "write the update's progress to Cloud Logging",
|
|
67
75
|
permissions: ["logging.logEntries.create"],
|
package/src/converge-job.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type ConvergeQueue,
|
|
20
20
|
type ConvergeUpgradeOutcome,
|
|
21
21
|
runConvergeJob,
|
|
22
|
+
type SecureDatabaseResult,
|
|
22
23
|
} from "./converge-job.js";
|
|
23
24
|
import type { ExecutionState } from "./install-job.js";
|
|
24
25
|
import type { InstallMetadata } from "./install-state.js";
|
|
@@ -147,6 +148,8 @@ function harness(
|
|
|
147
148
|
imageSigned?: boolean;
|
|
148
149
|
/** What IAM answers the self-check; every permission asked for by default. */
|
|
149
150
|
iam?: IamTester;
|
|
151
|
+
/** #1373 — the database TLS step's answer; null: the queue dials the DNS name already. */
|
|
152
|
+
secureDatabase?: SecureDatabaseResult | null;
|
|
150
153
|
} = {},
|
|
151
154
|
): Harness {
|
|
152
155
|
const calls: string[] = [];
|
|
@@ -156,6 +159,13 @@ function harness(
|
|
|
156
159
|
let stored = opts.checkpoint ?? null;
|
|
157
160
|
const deps: ConvergeJobDeps = {
|
|
158
161
|
queue: q.queue,
|
|
162
|
+
secureDatabase:
|
|
163
|
+
opts.secureDatabase === undefined || opts.secureDatabase === null
|
|
164
|
+
? null
|
|
165
|
+
: async () => {
|
|
166
|
+
calls.push("secure database");
|
|
167
|
+
return opts.secureDatabase as SecureDatabaseResult;
|
|
168
|
+
},
|
|
159
169
|
projectId: "acme",
|
|
160
170
|
region: "europe-west1",
|
|
161
171
|
iam: opts.iam ?? {
|
|
@@ -556,6 +566,66 @@ describe("runConvergeJob — resume after a killed run", () => {
|
|
|
556
566
|
});
|
|
557
567
|
});
|
|
558
568
|
|
|
569
|
+
describe("runConvergeJob — the database TLS step (#1373)", () => {
|
|
570
|
+
const DNS = "abc.p-label.europe-west1.sql-psa.goog";
|
|
571
|
+
|
|
572
|
+
it("a failed TLS step reaches the timeline and ends the request, before any roll", async () => {
|
|
573
|
+
const h = harness({
|
|
574
|
+
secureDatabase: {
|
|
575
|
+
ok: false,
|
|
576
|
+
error:
|
|
577
|
+
"database TLS could not be set up: caelo-production-pg-1 reports no PRIVATE_SERVICES_ACCESS DNS name",
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
const r = await runConvergeJob(h.deps);
|
|
581
|
+
expect(r.exitCode).toBe(1);
|
|
582
|
+
expect(r.status).toBe("failed");
|
|
583
|
+
expect(h.upgrades).toEqual([]);
|
|
584
|
+
// Under the lock, after the resumable checkpoint, released at the end.
|
|
585
|
+
expect(h.calls).toEqual([
|
|
586
|
+
"manifest 0.11.0",
|
|
587
|
+
"lock",
|
|
588
|
+
"checkpoint running",
|
|
589
|
+
"secure database",
|
|
590
|
+
"checkpoint failed",
|
|
591
|
+
"unlock",
|
|
592
|
+
]);
|
|
593
|
+
const end = h.q.ends[0];
|
|
594
|
+
expect(end?.codes).toEqual(["database-tls"]);
|
|
595
|
+
expect(end?.reason).toContain("reports no PRIVATE_SERVICES_ACCESS DNS name");
|
|
596
|
+
// The step's start and its error are timeline events of the request.
|
|
597
|
+
const converge = h.q.events.filter((e) => e.phase === "converge");
|
|
598
|
+
expect(converge[0]?.message).toStartWith("Setting up verified encryption");
|
|
599
|
+
const error = h.q.events.find((e) => e.level === "error");
|
|
600
|
+
expect(error?.message).toContain("reports no PRIVATE_SERVICES_ACCESS DNS name");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("a successful TLS step runs right before the upgrade and names the verified host", async () => {
|
|
604
|
+
const h = harness({ secureDatabase: { ok: true, host: DNS } });
|
|
605
|
+
const r = await runConvergeJob(h.deps);
|
|
606
|
+
expect(r.status).toBe("succeeded");
|
|
607
|
+
const at = h.calls.indexOf("secure database");
|
|
608
|
+
expect(at).toBeGreaterThan(h.calls.indexOf("lock"));
|
|
609
|
+
expect(h.calls[at + 1]).toBe("upgrade");
|
|
610
|
+
expect(h.q.events.some((e) => e.level === "success" && e.message.includes(DNS))).toBe(true);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("stage 1 never runs it: the release's own installer changes the database", async () => {
|
|
614
|
+
const h = harness({ ownVersion: "0.10.53", secureDatabase: { ok: true, host: DNS } });
|
|
615
|
+
await runConvergeJob(h.deps);
|
|
616
|
+
expect(h.calls.includes("secure database")).toBe(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("a stop before the lock leaves the database alone", async () => {
|
|
620
|
+
const h = harness({
|
|
621
|
+
secureDatabase: { ok: true, host: DNS },
|
|
622
|
+
manifest: { ok: false, error: { code: "signature", message: "no valid signature" } },
|
|
623
|
+
});
|
|
624
|
+
await runConvergeJob(h.deps);
|
|
625
|
+
expect(h.calls.includes("secure database")).toBe(false);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
559
629
|
describe("runConvergeJob — nothing to do", () => {
|
|
560
630
|
it("exits cleanly without a converge request", async () => {
|
|
561
631
|
const h = harness({ claim: null });
|
package/src/converge-job.ts
CHANGED
|
@@ -116,8 +116,22 @@ export type ConvergeUpgradeOutcome =
|
|
|
116
116
|
readonly unrecorded: boolean;
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
/** How {@link ConvergeJobDeps.secureDatabase} ended. */
|
|
120
|
+
export type SecureDatabaseResult =
|
|
121
|
+
| { readonly ok: true; readonly host: string }
|
|
122
|
+
| { readonly ok: false; readonly error: string };
|
|
123
|
+
|
|
119
124
|
export interface ConvergeJobDeps {
|
|
120
125
|
readonly queue: ConvergeQueue;
|
|
126
|
+
/**
|
|
127
|
+
* #1373 — the install's database TLS step (#1338, cloudsql-tls.ts), and
|
|
128
|
+
* then {@link queue} moved onto the database's DNS name with verify-full.
|
|
129
|
+
* Null when the queue already dials that name. Until it ran the queue
|
|
130
|
+
* dials the instance's IP (converge-db-trust.ts), so a failure of the
|
|
131
|
+
* step reaches the timeline and the request's end like any other step;
|
|
132
|
+
* when it fails the queue is reopened on whatever the install dials then.
|
|
133
|
+
*/
|
|
134
|
+
readonly secureDatabase: (() => Promise<SecureDatabaseResult>) | null;
|
|
121
135
|
/** From the metadata server — never from env or argv. */
|
|
122
136
|
readonly projectId: string;
|
|
123
137
|
/** The region this job runs in (metadata server). */
|
|
@@ -463,6 +477,24 @@ export async function runConvergeJob(deps: ConvergeJobDeps): Promise<UpgraderRun
|
|
|
463
477
|
save({});
|
|
464
478
|
|
|
465
479
|
r.phase("converge");
|
|
480
|
+
// #1373 — stage 2 only, under the lock and after every check: it
|
|
481
|
+
// changes the database instance, its DNS and the services' URLs.
|
|
482
|
+
if (deps.secureDatabase) {
|
|
483
|
+
r.info(
|
|
484
|
+
"Setting up verified encryption for the database connection (the database's certificate and its private DNS name).",
|
|
485
|
+
);
|
|
486
|
+
await r.flush();
|
|
487
|
+
const secured = await deps.secureDatabase();
|
|
488
|
+
if (!secured.ok) {
|
|
489
|
+
save({ state: "failed", phase: "stopped" });
|
|
490
|
+
return stopWith(
|
|
491
|
+
"failed",
|
|
492
|
+
["database-tls"],
|
|
493
|
+
`The update to ${m.version} stopped at the database's TLS step, before it rolled anything: ${secured.error}. The previous release keeps serving. Fix what it names and start the update again; it continues where it stopped.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
r.success(`The database connection is verified (${secured.host}).`);
|
|
497
|
+
}
|
|
466
498
|
const outcome = await deps.upgrade({
|
|
467
499
|
manifest: m,
|
|
468
500
|
reporter: r,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1338 — every database URL a provider adapter builds asks for
|
|
5
|
+
* `sslmode=verify-full`, and no adapter still builds a weaker one
|
|
6
|
+
* (CLAUDE.md §2 no fallbacks: a downgrade is never silent).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { databaseTlsProblem } from "@caelo-cms/shared";
|
|
13
|
+
import { generateDockerCompose } from "./compose.js";
|
|
14
|
+
import { databaseUrls } from "./stack-contract.js";
|
|
15
|
+
|
|
16
|
+
const DNS = "abc123.p-label.europe-west1.sql-psa.goog";
|
|
17
|
+
|
|
18
|
+
describe("database URL builders (#1338)", () => {
|
|
19
|
+
it("Cloud SQL: verify-full on the instance's DNS name, never on its IP", () => {
|
|
20
|
+
for (const url of Object.values(databaseUrls(DNS))) {
|
|
21
|
+
expect(databaseTlsProblem(url)).toBeNull();
|
|
22
|
+
expect(new URL(url).hostname).toBe(DNS);
|
|
23
|
+
}
|
|
24
|
+
expect(() => databaseUrls("10.20.0.3")).toThrow(/DNS name/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("self-hosted Compose: every service URL is verify-full against the install's CA", () => {
|
|
28
|
+
const yaml = generateDockerCompose({
|
|
29
|
+
domain: "example.com",
|
|
30
|
+
postgresPassword: "superpw",
|
|
31
|
+
rolePasswords: { admin: "adminpw", public: "publicpw", publicStaging: "ps" },
|
|
32
|
+
caeloSecretKek: "0".repeat(64),
|
|
33
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
34
|
+
internalSecret: "i".repeat(48),
|
|
35
|
+
diskSize: "20Gi",
|
|
36
|
+
images: {
|
|
37
|
+
mode: "release",
|
|
38
|
+
admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
|
|
39
|
+
gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
|
|
40
|
+
},
|
|
41
|
+
ports: { http: 80, https: 443 },
|
|
42
|
+
project: "caelo-example-com",
|
|
43
|
+
});
|
|
44
|
+
const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
|
|
45
|
+
// admin: cms_admin, cms_public, cms_public_staging (#1329); gateway: cms_public.
|
|
46
|
+
expect(urls).toHaveLength(4);
|
|
47
|
+
for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const stack of ["gcp", "gcp-firebase", "aws", "azure"]) {
|
|
51
|
+
it(`stacks/${stack} builds no URL weaker than verify-full`, () => {
|
|
52
|
+
const src = readFileSync(resolve(import.meta.dir, `../stacks/${stack}/index.ts`), "utf8");
|
|
53
|
+
expect(src).not.toMatch(/sslmode=(disable|allow|prefer|require|verify-ca)\b/);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|