@caelo-cms/provisioning 0.11.10 → 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/adopt-existing.d.ts +6 -2
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +57 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +5 -2
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -1
- package/dist/agent/command.js.map +1 -1
- package/dist/agent-db.js +25 -8
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +59 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +30 -0
- package/dist/cloudsql-tls.d.ts.map +1 -1
- package/dist/cloudsql-tls.js +7 -1
- package/dist/cloudsql-tls.js.map +1 -1
- package/dist/compose.d.ts +14 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +54 -1
- package/dist/compose.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 +9 -2
- 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/gcp-names.d.ts +13 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +22 -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 +7 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +3 -0
- package/dist/lifecycle.js.map +1 -1
- 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/runtime-secrets.d.ts +2 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +19 -15
- 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/self-hosted-roles.d.ts +16 -9
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +49 -15
- 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-upgrade.d.ts +7 -4
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +9 -5
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +67 -6
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +87 -6
- package/dist/stack-contract.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/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +110 -10
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +6 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +3 -0
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +5 -4
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +5 -4
- package/dist/upgrader-gcp.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 +81 -11
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +87 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-memory.test.ts +1 -1
- package/src/adopt-existing.test.ts +46 -0
- package/src/adopt-existing.ts +58 -2
- package/src/agent/command.ts +6 -3
- package/src/caddy-redirects.test.ts +1 -1
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +63 -8
- package/src/cloudsql-tls.test.ts +30 -0
- package/src/cloudsql-tls.ts +10 -3
- package/src/compose.ts +68 -0
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +9 -1
- 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 +3 -2
- package/src/deployed-release.test.ts +3 -0
- package/src/gateway-env-isolation.test.ts +40 -7
- package/src/gcp-names.ts +25 -0
- package/src/index.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +22 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access.test.ts +15 -0
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +38 -13
- 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/runtime-secrets.test.ts +18 -8
- package/src/runtime-secrets.ts +37 -18
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +72 -1
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +49 -19
- package/src/self-hosted-roles.ts +60 -20
- 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 +1 -0
- package/src/self-hosted-upgrade.test.ts +2 -1
- package/src/self-hosted-upgrade.ts +14 -8
- package/src/stack-contract.test.ts +31 -3
- package/src/stack-contract.ts +113 -6
- package/src/stack-converge-secrets.test.ts +2 -0
- package/src/stack-converge.test.ts +6 -4
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/upgrade/converge.ts +130 -9
- package/src/upgrade/upgrade.test.ts +85 -2
- package/src/upgrade/upgrade.ts +9 -0
- package/src/upgrader-gcp.ts +5 -4
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +101 -11
- package/src/wizards/gcp.ts +109 -8
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +41 -1
- package/stacks/azure/index.ts +51 -3
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +50 -1
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +50 -1
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +4 -0
- package/stacks/self-hosted/index.ts +29 -4
|
@@ -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
|
@@ -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,
|
|
@@ -355,6 +362,7 @@ export function gcpConvergeDeps(
|
|
|
355
362
|
|
|
356
363
|
return {
|
|
357
364
|
queue,
|
|
365
|
+
secureDatabase,
|
|
358
366
|
projectId,
|
|
359
367
|
region,
|
|
360
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,
|
|
@@ -28,7 +28,7 @@ describe("database URL builders (#1338)", () => {
|
|
|
28
28
|
const yaml = generateDockerCompose({
|
|
29
29
|
domain: "example.com",
|
|
30
30
|
postgresPassword: "superpw",
|
|
31
|
-
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
31
|
+
rolePasswords: { admin: "adminpw", public: "publicpw", publicStaging: "ps" },
|
|
32
32
|
caeloSecretKek: "0".repeat(64),
|
|
33
33
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
34
34
|
internalSecret: "i".repeat(48),
|
|
@@ -42,7 +42,8 @@ describe("database URL builders (#1338)", () => {
|
|
|
42
42
|
project: "caelo-example-com",
|
|
43
43
|
});
|
|
44
44
|
const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
|
|
45
|
-
|
|
45
|
+
// admin: cms_admin, cms_public, cms_public_staging (#1329); gateway: cms_public.
|
|
46
|
+
expect(urls).toHaveLength(4);
|
|
46
47
|
for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
|
|
47
48
|
});
|
|
48
49
|
|
|
@@ -128,6 +128,9 @@ describe("checkDeployedRuntimeEnv", () => {
|
|
|
128
128
|
const adminOk = svc([
|
|
129
129
|
secret("ADMIN_DATABASE_PASSWORD"),
|
|
130
130
|
secret("PUBLIC_ADMIN_DATABASE_PASSWORD"),
|
|
131
|
+
// #1329 — the staging database's pool: a release from before it is
|
|
132
|
+
// refused here too and goes through `upgrade` first.
|
|
133
|
+
secret("PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD"),
|
|
131
134
|
]);
|
|
132
135
|
const gatewayOk = svc([secret("PUBLIC_DATABASE_PASSWORD", "public-role-password")]);
|
|
133
136
|
const proxyOk = svc([secret("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]);
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* self-hosted the generated compose gateway service: env, secrets,
|
|
17
17
|
* mounts
|
|
18
18
|
*
|
|
19
|
+
* Issue #1329 — the same crosswise between the two public databases: the
|
|
20
|
+
* live gateway's env names nothing of the staging database
|
|
21
|
+
* (cms_public_staging, public_staging_role, its password secret), and its
|
|
22
|
+
* identity cannot read the staging login's password, on every adapter. The
|
|
23
|
+
* staging gateway (#1330) gets the mirror-image checks with its service.
|
|
24
|
+
*
|
|
19
25
|
* The gateway itself refuses to boot on such an env
|
|
20
26
|
* (apps/api-gateway `adminDatabaseAccessInEnv`); this catches it at review
|
|
21
27
|
* time, before a release can ship it.
|
|
@@ -42,6 +48,12 @@ const ADMIN_ACCESS = /cms_admin|admin_role|gateway_role|ADMIN_DATABASE|GATEWAY_D
|
|
|
42
48
|
/** Secrets that unlock a cms_admin login. */
|
|
43
49
|
const ADMIN_SECRETS = ["postgres-password", "gateway-role-password"];
|
|
44
50
|
|
|
51
|
+
/** #1329 — anything in the live gateway's env that reaches the staging database. */
|
|
52
|
+
const STAGING_ACCESS = /cms_public_staging|public_staging_role|PUBLIC_STAGING/;
|
|
53
|
+
|
|
54
|
+
/** #1329 — the secret that unlocks the staging gateway's login. */
|
|
55
|
+
const STAGING_SECRETS = ["public-staging-role-password"];
|
|
56
|
+
|
|
45
57
|
/** The secrets the gateway reads on GCP: its own login and the staging gate key. */
|
|
46
58
|
const GCP_GATEWAY_SECRETS = [
|
|
47
59
|
"public-role-password",
|
|
@@ -68,11 +80,19 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
|
|
|
68
80
|
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
69
81
|
});
|
|
70
82
|
|
|
71
|
-
it("carries no cms_admin URL, role or credential name", () => {
|
|
83
|
+
it("carries no cms_admin URL, role or credential name, and nothing of the staging database (#1329)", () => {
|
|
72
84
|
for (const v of env) {
|
|
73
85
|
expect(v.name).not.toMatch(ADMIN_ACCESS);
|
|
74
|
-
|
|
86
|
+
expect(v.name).not.toMatch(STAGING_ACCESS);
|
|
87
|
+
if ("value" in v && typeof v.value === "string") {
|
|
88
|
+
expect(v.value).not.toMatch(ADMIN_ACCESS);
|
|
89
|
+
expect(v.value).not.toMatch(STAGING_ACCESS);
|
|
90
|
+
}
|
|
75
91
|
}
|
|
92
|
+
expect(env.find((v) => v.name === "CAELO_GATEWAY_ENVIRONMENT")).toEqual({
|
|
93
|
+
name: "CAELO_GATEWAY_ENVIRONMENT",
|
|
94
|
+
value: "live",
|
|
95
|
+
});
|
|
76
96
|
expect(env.find((v) => v.name === "PUBLIC_DATABASE_URL")).toEqual({
|
|
77
97
|
name: "PUBLIC_DATABASE_URL",
|
|
78
98
|
value:
|
|
@@ -80,12 +100,16 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
|
|
|
80
100
|
});
|
|
81
101
|
});
|
|
82
102
|
|
|
83
|
-
it("references no secret that unlocks a cms_admin login", () => {
|
|
103
|
+
it("references no secret that unlocks a cms_admin or the staging login", () => {
|
|
84
104
|
for (const secret of Object.values(SERVICE_SECRET_ENV.gateway)) {
|
|
85
105
|
expect(ADMIN_SECRETS).not.toContain(secret);
|
|
106
|
+
expect(STAGING_SECRETS).not.toContain(secret);
|
|
86
107
|
}
|
|
87
108
|
const gatewayBindings = runtimeSecretBindings().filter((b) => b.service === "gateway");
|
|
88
|
-
for (const b of gatewayBindings)
|
|
109
|
+
for (const b of gatewayBindings) {
|
|
110
|
+
expect(ADMIN_SECRETS).not.toContain(b.secret);
|
|
111
|
+
expect(STAGING_SECRETS).not.toContain(b.secret);
|
|
112
|
+
}
|
|
89
113
|
});
|
|
90
114
|
|
|
91
115
|
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
@@ -134,7 +158,7 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
134
158
|
const out = generateDockerCompose({
|
|
135
159
|
domain: "example.com",
|
|
136
160
|
postgresPassword: "superpw",
|
|
137
|
-
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
161
|
+
rolePasswords: { admin: "adminpw", public: "publicpw", publicStaging: "stagingpw" },
|
|
138
162
|
caeloSecretKek: "0".repeat(64),
|
|
139
163
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
140
164
|
internalSecret: "i".repeat(48),
|
|
@@ -152,7 +176,9 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
152
176
|
const gateway = out.slice(start, end === -1 ? undefined : end);
|
|
153
177
|
expect(gateway).toContain("PUBLIC_DATABASE_URL:");
|
|
154
178
|
expect(gateway).not.toMatch(ADMIN_ACCESS);
|
|
155
|
-
expect(gateway).not.toMatch(
|
|
179
|
+
expect(gateway).not.toMatch(STAGING_ACCESS);
|
|
180
|
+
expect(gateway).not.toMatch(/superpw|adminpw|gatewaypw|stagingpw/);
|
|
181
|
+
expect(gateway).toContain('CAELO_GATEWAY_ENVIRONMENT: "live"');
|
|
156
182
|
// No other way in: no Compose secrets, no env file, and exactly one
|
|
157
183
|
// mount — the database CA certificate (#1338), public, no credential.
|
|
158
184
|
expect(gateway).not.toMatch(/^ {4}(secrets|env_file):/m);
|
|
@@ -164,6 +190,8 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
164
190
|
expect(keys.sort()).toEqual([
|
|
165
191
|
// #1254 — its cms_public pool size (pool-budget-stack.ts).
|
|
166
192
|
"CAELO_DB_PUBLIC_POOL_MAX",
|
|
193
|
+
// #1329 — the live site's gateway.
|
|
194
|
+
"CAELO_GATEWAY_ENVIRONMENT",
|
|
167
195
|
"CAELO_TRUSTED_PROXY_HOPS",
|
|
168
196
|
"GATEWAY_PORT",
|
|
169
197
|
"NODE_ENV",
|
|
@@ -181,6 +209,9 @@ describe("azure: the gateway container app (#635)", () => {
|
|
|
181
209
|
const block = src.slice(start, src.indexOf("});", start));
|
|
182
210
|
expect(block).toContain('{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }');
|
|
183
211
|
expect(block).not.toMatch(/adminDatabase|gatewayDatabase|ADMIN_DATABASE|GATEWAY_DATABASE/);
|
|
212
|
+
// #1329 — nothing of the staging database, and it says it is the live one.
|
|
213
|
+
expect(block).not.toMatch(/publicStaging|PUBLIC_STAGING/);
|
|
214
|
+
expect(block).toContain('{ name: GATEWAY_ENVIRONMENT_ENV, value: "live" }');
|
|
184
215
|
});
|
|
185
216
|
|
|
186
217
|
it("builds no cms_admin URL for any role but the admin's", () => {
|
|
@@ -206,7 +237,9 @@ describe("aws: the stack (#635)", () => {
|
|
|
206
237
|
|
|
207
238
|
it("the gateway's task role reads only the public_role password (review finding)", () => {
|
|
208
239
|
expect(AWS_SERVICE_SECRETS.gateway).toEqual(["public-role-password"]);
|
|
209
|
-
for (const secret of ADMIN_SECRETS)
|
|
240
|
+
for (const secret of [...ADMIN_SECRETS, ...STAGING_SECRETS]) {
|
|
241
|
+
expect(AWS_SERVICE_SECRETS.gateway).not.toContain(secret);
|
|
242
|
+
}
|
|
210
243
|
});
|
|
211
244
|
|
|
212
245
|
it("each service gets its own task role whose policy is built from that table", () => {
|
package/src/gcp-names.ts
CHANGED
|
@@ -221,6 +221,31 @@ export function captureServiceAccountEmail(projectId: string, env: string): stri
|
|
|
221
221
|
return `${captureServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* #1140 — account id of the scheduler tick's service account: signs its own
|
|
226
|
+
* IAP credential and calls the admin's tick endpoint, nothing else
|
|
227
|
+
* (scheduler-setup.ts). `caelo-production-scheduler` is 26 of the 30
|
|
228
|
+
* characters an account id may have.
|
|
229
|
+
*/
|
|
230
|
+
export function schedulerServiceAccountId(env: string): string {
|
|
231
|
+
return `${gcpNamePrefix(env)}-scheduler`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Email of the scheduler tick's service account. */
|
|
235
|
+
export function schedulerServiceAccountEmail(projectId: string, env: string): string {
|
|
236
|
+
return `${schedulerServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** #1140 — the Cloud Workflows workflow one scheduler tick runs (scheduler-setup.ts). */
|
|
240
|
+
export function schedulerWorkflowName(env: string): string {
|
|
241
|
+
return `${gcpNamePrefix(env)}-scheduler-tick`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** #1140 — the Cloud Scheduler job that starts the tick workflow every N minutes. */
|
|
245
|
+
export function schedulerJobName(env: string): string {
|
|
246
|
+
return `${gcpNamePrefix(env)}-scheduler-tick`;
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
/**
|
|
225
250
|
* Account id of the upgrader job's own service account: may roll the two
|
|
226
251
|
* Cloud Run services, run the migration job and back up Cloud SQL, never
|
package/src/index.test.ts
CHANGED
|
@@ -152,7 +152,7 @@ describe("docker-compose generator", () => {
|
|
|
152
152
|
const out = generateDockerCompose({
|
|
153
153
|
domain: "example.com",
|
|
154
154
|
postgresPassword: "supersecret",
|
|
155
|
-
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
155
|
+
rolePasswords: { admin: "adminpw", public: "publicpw", publicStaging: "ps" },
|
|
156
156
|
caeloSecretKek: "0".repeat(64),
|
|
157
157
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
158
158
|
internalSecret: "i".repeat(48),
|
|
@@ -175,7 +175,7 @@ describe("docker-compose generator", () => {
|
|
|
175
175
|
const out = generateDockerCompose({
|
|
176
176
|
domain: "example.com",
|
|
177
177
|
postgresPassword: "supersecret",
|
|
178
|
-
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
178
|
+
rolePasswords: { admin: "adminpw", public: "publicpw", publicStaging: "ps" },
|
|
179
179
|
caeloSecretKek: "0".repeat(64),
|
|
180
180
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
181
181
|
internalSecret: "i".repeat(48),
|
|
@@ -201,7 +201,7 @@ describe("docker-compose generator", () => {
|
|
|
201
201
|
const out = generateDockerCompose({
|
|
202
202
|
domain: "example.com",
|
|
203
203
|
postgresPassword: 'pa"ss',
|
|
204
|
-
rolePasswords: { admin: "a", public: "b" },
|
|
204
|
+
rolePasswords: { admin: "a", public: "b", publicStaging: "ps" },
|
|
205
205
|
caeloSecretKek: "0".repeat(64),
|
|
206
206
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
207
207
|
internalSecret: "i".repeat(48),
|
package/src/index.ts
CHANGED
|
@@ -46,12 +46,18 @@ export {
|
|
|
46
46
|
selfHostedCdnCopy,
|
|
47
47
|
} from "./cdn-copy.js";
|
|
48
48
|
export { type ComposeSpec, generateDockerCompose, type RolePasswords } from "./compose.js";
|
|
49
|
+
export {
|
|
50
|
+
type ConvergeDatabaseTarget,
|
|
51
|
+
convergeDatabaseTarget,
|
|
52
|
+
serverIdentityProblem,
|
|
53
|
+
} from "./converge-db-trust.js";
|
|
49
54
|
export {
|
|
50
55
|
type ConvergeClaim,
|
|
51
56
|
type ConvergeJobDeps,
|
|
52
57
|
type ConvergeQueue,
|
|
53
58
|
type ConvergeUpgradeOutcome,
|
|
54
59
|
runConvergeJob,
|
|
60
|
+
type SecureDatabaseResult,
|
|
55
61
|
} from "./converge-job.js";
|
|
56
62
|
export { convergeExecutionNameError, gcpConvergeDeps } from "./converge-job-gcp.js";
|
|
57
63
|
export { INFRA_CONTRACT_VERSION } from "./infra-contract.js";
|