@caelo-cms/provisioning 0.10.46 → 0.10.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adc-impersonation.d.ts.map +1 -1
- package/dist/adc-impersonation.js +23 -1
- package/dist/adc-impersonation.js.map +1 -1
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +122 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +31 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -5
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-attach.d.ts +23 -0
- package/dist/state-attach.d.ts.map +1 -0
- package/dist/state-attach.js +112 -0
- package/dist/state-attach.js.map +1 -0
- package/dist/state-backend.d.ts +11 -0
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +20 -0
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +20 -0
- package/dist/state-command.d.ts.map +1 -0
- package/dist/state-command.js +237 -0
- package/dist/state-command.js.map +1 -0
- package/dist/state-migrate.d.ts +121 -0
- package/dist/state-migrate.d.ts.map +1 -0
- package/dist/state-migrate.js +290 -0
- package/dist/state-migrate.js.map +1 -0
- package/dist/state-pulumi.d.ts +17 -0
- package/dist/state-pulumi.d.ts.map +1 -0
- package/dist/state-pulumi.js +218 -0
- package/dist/state-pulumi.js.map +1 -0
- package/dist/state-store.d.ts +27 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +65 -0
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +9 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +8 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +291 -83
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upgrade of a v0.10.29 install — database passwords in plain env vars,
|
|
5
|
+
* the gateway holding the KEK, no internal/tool-approval secret — and of a
|
|
6
|
+
* v0.10.35 install — Secret Manager references, but the gateway still on an
|
|
7
|
+
* admin_role pool (#613) — onto the current env contract. A fake Cloud Run applies the
|
|
8
|
+
* `services update` flags with gcloud's semantics (all literal env changes
|
|
9
|
+
* before the secret changes, a type switch inside one kind rejected) and
|
|
10
|
+
* records one revision per update, so the tests can assert that no revision
|
|
11
|
+
* is ever without a var and that the composed database URLs still reach the
|
|
12
|
+
* same database.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from "bun:test";
|
|
16
|
+
import { databaseUrlFromEnv } from "@caelo-cms/shared";
|
|
17
|
+
import type { CloudRunSlug } from "./stack-contract.js";
|
|
18
|
+
import {
|
|
19
|
+
type LiveEnvValue,
|
|
20
|
+
liveContainerEnv,
|
|
21
|
+
liveEnvHasInlinePassword,
|
|
22
|
+
planContractEnv,
|
|
23
|
+
planEnvUpdate,
|
|
24
|
+
serviceRollArgs,
|
|
25
|
+
} from "./stack-converge.js";
|
|
26
|
+
|
|
27
|
+
const PW = "4f2a9c0d1e2b3a4f5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b";
|
|
28
|
+
const HOST = "10.20.0.3";
|
|
29
|
+
|
|
30
|
+
/** Secret Manager as the fake revisions resolve it. */
|
|
31
|
+
const SECRET_VALUES: Record<string, string> = {
|
|
32
|
+
"caelo-production-postgres-password": PW,
|
|
33
|
+
"caelo-production-secret-kek": "k".repeat(64),
|
|
34
|
+
"caelo-production-internal-secret": "i".repeat(64),
|
|
35
|
+
"caelo-production-tool-approval-secret": "t".repeat(64),
|
|
36
|
+
// #613 — the gateway's own role passwords (CLI-generated).
|
|
37
|
+
"caelo-production-public-role-password": "p".repeat(64),
|
|
38
|
+
"caelo-production-gateway-role-password": "g".repeat(64),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Env vars that would hand the gateway an admin_role credential (apps/api-gateway ADMIN_CREDENTIAL_ENV). */
|
|
42
|
+
const ADMIN_CREDENTIAL_ENV = [
|
|
43
|
+
"ADMIN_DATABASE_URL",
|
|
44
|
+
"ADMIN_DATABASE_PASSWORD",
|
|
45
|
+
"PUBLIC_ADMIN_DATABASE_URL",
|
|
46
|
+
"PUBLIC_ADMIN_DATABASE_PASSWORD",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Split a gcloud list flag value, honouring the `^<delim>^` escape. */
|
|
50
|
+
function splitList(value: string): string[] {
|
|
51
|
+
const escaped = value.match(/^\^(.)\^([\s\S]*)$/);
|
|
52
|
+
return escaped ? (escaped[2] as string).split(escaped[1] as string) : value.split(",");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A Cloud Run service's container env, updated with gcloud's semantics
|
|
57
|
+
* (googlecloudsdk/command_lib/run: EnvVarLiteralChanges applies before
|
|
58
|
+
* SecretEnvVarChanges; each rejects a name already set with the other type).
|
|
59
|
+
*/
|
|
60
|
+
class FakeService {
|
|
61
|
+
readonly revisions: Map<string, LiveEnvValue>[] = [];
|
|
62
|
+
serviceAccount: string;
|
|
63
|
+
|
|
64
|
+
constructor(env: Map<string, LiveEnvValue>, serviceAccount: string) {
|
|
65
|
+
this.revisions.push(env);
|
|
66
|
+
this.serviceAccount = serviceAccount;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get env(): Map<string, LiveEnvValue> {
|
|
70
|
+
return this.revisions[this.revisions.length - 1] as Map<string, LiveEnvValue>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
update(args: readonly string[]): void {
|
|
74
|
+
const flag = (name: string) =>
|
|
75
|
+
args
|
|
76
|
+
.filter((a) => a.startsWith(`${name}=`))
|
|
77
|
+
.flatMap((a) => splitList(a.slice(name.length + 1)));
|
|
78
|
+
const env = new Map(this.env);
|
|
79
|
+
// 1. literal changes: removals, then updates
|
|
80
|
+
for (const name of flag("--remove-env-vars")) {
|
|
81
|
+
if (env.get(name)?.kind === "value") env.delete(name);
|
|
82
|
+
}
|
|
83
|
+
for (const pair of flag("--update-env-vars")) {
|
|
84
|
+
const [name, ...rest] = pair.split("=");
|
|
85
|
+
if (env.get(name as string)?.kind === "secret") {
|
|
86
|
+
throw new Error(`Cannot update environment variable [${name}] to string literal`);
|
|
87
|
+
}
|
|
88
|
+
env.set(name as string, { kind: "value", value: rest.join("=") });
|
|
89
|
+
}
|
|
90
|
+
// 2. secret changes: removals, then updates
|
|
91
|
+
for (const name of flag("--remove-secrets")) {
|
|
92
|
+
if (env.get(name)?.kind === "secret") env.delete(name);
|
|
93
|
+
}
|
|
94
|
+
for (const pair of flag("--update-secrets")) {
|
|
95
|
+
const [name, ref] = pair.split("=") as [string, string];
|
|
96
|
+
if (env.get(name)?.kind === "value") {
|
|
97
|
+
throw new Error(`Cannot update environment variable [${name}] to the given type`);
|
|
98
|
+
}
|
|
99
|
+
const [secret, version] = ref.split(":") as [string, string];
|
|
100
|
+
env.set(name, { kind: "secret", secret, version });
|
|
101
|
+
}
|
|
102
|
+
const sa = args.find((a) => a.startsWith("--service-account="));
|
|
103
|
+
if (sa) this.serviceAccount = sa.slice("--service-account=".length);
|
|
104
|
+
this.revisions.push(env);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The process env a revision's container sees (secrets resolved). */
|
|
109
|
+
function containerEnv(env: ReadonlyMap<string, LiveEnvValue>): Record<string, string> {
|
|
110
|
+
const out: Record<string, string> = {};
|
|
111
|
+
for (const [name, v] of env) {
|
|
112
|
+
out[name] = v.kind === "value" ? v.value : (SECRET_VALUES[v.secret] ?? "<missing secret>");
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `user:password@host/db?query` of a composed URL. */
|
|
118
|
+
function credentials(url: string | undefined): string {
|
|
119
|
+
if (!url) throw new Error("no URL");
|
|
120
|
+
const u = new URL(url);
|
|
121
|
+
return `${u.username}:${u.password}@${u.host}${u.pathname}${u.search}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The gateway's process env after the upgrade: gateway_role on cms_admin and
|
|
126
|
+
* public_role on cms_public, each with its own secret, and no admin_role
|
|
127
|
+
* credential at all (the gateway refuses to boot with one).
|
|
128
|
+
*/
|
|
129
|
+
function expectGatewayOnItsOwnLogins(env: Record<string, string>): void {
|
|
130
|
+
for (const name of ADMIN_CREDENTIAL_ENV) expect(Object.keys(env)).not.toContain(name);
|
|
131
|
+
expect(Object.values(env).join("\n")).not.toContain(PW);
|
|
132
|
+
expect(credentials(databaseUrlFromEnv(["GATEWAY_DATABASE_URL"], env))).toBe(
|
|
133
|
+
`gateway_role:${"g".repeat(64)}@${HOST}:5432/cms_admin?sslmode=require`,
|
|
134
|
+
);
|
|
135
|
+
expect(credentials(databaseUrlFromEnv(["PUBLIC_DATABASE_URL"], env))).toBe(
|
|
136
|
+
`public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=require`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const plainEnv = (entries: Record<string, string>) =>
|
|
141
|
+
Object.entries(entries).map(([name, value]) => ({ name, value }));
|
|
142
|
+
const secretRef = (name: string, secret: string) => ({
|
|
143
|
+
name,
|
|
144
|
+
valueFrom: { secretKeyRef: { name: secret, key: "latest" } },
|
|
145
|
+
});
|
|
146
|
+
const serviceJson = (env: unknown[]) =>
|
|
147
|
+
JSON.stringify({ spec: { template: { spec: { containers: [{ env }] } } } });
|
|
148
|
+
|
|
149
|
+
/** What `gcloud run services describe` showed on a live v0.10.29 gcp install. */
|
|
150
|
+
function v01029Install(scheme: "postgresql" | "postgres") {
|
|
151
|
+
const url = (role: string, db: string) =>
|
|
152
|
+
`${scheme}://${role}:${PW}@${HOST}:5432/${db}?sslmode=require`;
|
|
153
|
+
const common = {
|
|
154
|
+
CAELO_PROVIDER: "gcp",
|
|
155
|
+
CAELO_ENV: "production",
|
|
156
|
+
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
157
|
+
};
|
|
158
|
+
const admin = liveContainerEnv(
|
|
159
|
+
serviceJson([
|
|
160
|
+
...plainEnv({
|
|
161
|
+
...common,
|
|
162
|
+
CAELO_SITE_URL: "https://acme.com",
|
|
163
|
+
CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
|
|
164
|
+
CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
165
|
+
CAELO_STATIC_BUCKET: "acme-caelo-production-static",
|
|
166
|
+
CAELO_STAGING_BUCKET: "acme-caelo-production-staging",
|
|
167
|
+
ADMIN_DATABASE_URL: url("admin_role", "cms_admin"),
|
|
168
|
+
}),
|
|
169
|
+
secretRef("CAELO_SECRET_KEK", "caelo-production-secret-kek"),
|
|
170
|
+
...plainEnv({ PUBLIC_ADMIN_DATABASE_URL: url("admin_role", "cms_public") }),
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
const gateway = liveContainerEnv(
|
|
174
|
+
serviceJson([
|
|
175
|
+
...plainEnv({ ...common, ADMIN_DATABASE_URL: url("admin_role", "cms_admin") }),
|
|
176
|
+
secretRef("CAELO_SECRET_KEK", "caelo-production-secret-kek"),
|
|
177
|
+
...plainEnv({ PUBLIC_DATABASE_URL: url("public_role", "cms_public") }),
|
|
178
|
+
]),
|
|
179
|
+
);
|
|
180
|
+
const runSa = "caelo-production-run-sa@acme.iam.gserviceaccount.com";
|
|
181
|
+
return {
|
|
182
|
+
admin: new FakeService(admin, runSa),
|
|
183
|
+
gateway: new FakeService(gateway, runSa),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const INSTALL = {
|
|
188
|
+
provider: "gcp" as const,
|
|
189
|
+
projectId: "acme",
|
|
190
|
+
env: "production",
|
|
191
|
+
domain: "acme.com",
|
|
192
|
+
stagingHost: "staging.acme.com",
|
|
193
|
+
region: "europe-west1",
|
|
194
|
+
};
|
|
195
|
+
const SERVICE_ACCOUNTS: Record<CloudRunSlug, string> = {
|
|
196
|
+
admin: "caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
197
|
+
gateway: "caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Plan + apply one upgrade roll on the fake services, as upgradeCommand does. */
|
|
201
|
+
function upgrade(services: Record<CloudRunSlug, FakeService>) {
|
|
202
|
+
const plan = planContractEnv(INSTALL, {
|
|
203
|
+
admin: { serviceName: "adm", liveEnv: services.admin.env },
|
|
204
|
+
gateway: { serviceName: "gw", liveEnv: services.gateway.env },
|
|
205
|
+
});
|
|
206
|
+
if (!plan.ok) throw new Error(plan.error);
|
|
207
|
+
for (const slug of ["admin", "gateway"] as const) {
|
|
208
|
+
services[slug].update(
|
|
209
|
+
serviceRollArgs({
|
|
210
|
+
serviceName: slug,
|
|
211
|
+
region: INSTALL.region,
|
|
212
|
+
projectId: INSTALL.projectId,
|
|
213
|
+
imageRef: `${slug}@sha256:new`,
|
|
214
|
+
serviceAccount: SERVICE_ACCOUNTS[slug],
|
|
215
|
+
envFlags: plan.services[slug].flags,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return plan;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
describe.each(["postgresql", "postgres"] as const)(
|
|
223
|
+
"upgrading a v0.10.29 install (%s:// URLs) onto Secret Manager",
|
|
224
|
+
(scheme) => {
|
|
225
|
+
it("moves every database password out of the plain env in one revision per service", () => {
|
|
226
|
+
const services = v01029Install(scheme);
|
|
227
|
+
const plan = upgrade(services);
|
|
228
|
+
for (const slug of ["admin", "gateway"] as const) {
|
|
229
|
+
// One update → exactly one new revision.
|
|
230
|
+
expect(services[slug].revisions).toHaveLength(2);
|
|
231
|
+
for (const [name, v] of services[slug].env) {
|
|
232
|
+
if (v.kind === "value") expect(`${name}=${v.value}`).not.toContain(PW);
|
|
233
|
+
}
|
|
234
|
+
// The flags and what upgrade prints never carry the password either.
|
|
235
|
+
expect(JSON.stringify(plan)).not.toContain(PW);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("the admin composes the same credentials; the gateway its own two logins (#613)", () => {
|
|
240
|
+
const services = v01029Install(scheme);
|
|
241
|
+
const before = {
|
|
242
|
+
admin: containerEnv(services.admin.env),
|
|
243
|
+
gateway: containerEnv(services.gateway.env),
|
|
244
|
+
};
|
|
245
|
+
upgrade(services);
|
|
246
|
+
const after = {
|
|
247
|
+
admin: containerEnv(services.admin.env),
|
|
248
|
+
gateway: containerEnv(services.gateway.env),
|
|
249
|
+
};
|
|
250
|
+
for (const vars of [
|
|
251
|
+
["ADMIN_DATABASE_URL"],
|
|
252
|
+
["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"],
|
|
253
|
+
]) {
|
|
254
|
+
expect(credentials(databaseUrlFromEnv(vars, after.admin))).toBe(
|
|
255
|
+
credentials(databaseUrlFromEnv(vars, before.admin)),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
expectGatewayOnItsOwnLogins(after.gateway);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("mounts the internal + tool-approval secrets on the admin and takes the KEK off the gateway", () => {
|
|
262
|
+
const services = v01029Install(scheme);
|
|
263
|
+
upgrade(services);
|
|
264
|
+
const admin = containerEnv(services.admin.env);
|
|
265
|
+
expect(admin.CAELO_INTERNAL_SECRET).toBe("i".repeat(64));
|
|
266
|
+
expect(admin.CAELO_TOOL_APPROVAL_SECRET).toBe("t".repeat(64));
|
|
267
|
+
expect(admin.CAELO_SECRET_KEK).toBe("k".repeat(64));
|
|
268
|
+
const gateway = containerEnv(services.gateway.env);
|
|
269
|
+
expect(Object.keys(gateway)).not.toContain("CAELO_SECRET_KEK");
|
|
270
|
+
expect(Object.keys(gateway)).not.toContain("CAELO_INTERNAL_SECRET");
|
|
271
|
+
expect(services.gateway.serviceAccount).toBe(SERVICE_ACCOUNTS.gateway);
|
|
272
|
+
expect(services.admin.serviceAccount).toBe(SERVICE_ACCOUNTS.admin);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("is idempotent: a second upgrade changes no env", () => {
|
|
276
|
+
const services = v01029Install(scheme);
|
|
277
|
+
upgrade(services);
|
|
278
|
+
const again = upgrade(services);
|
|
279
|
+
expect(again.ok && again.services.admin.flags).toEqual([]);
|
|
280
|
+
expect(again.ok && again.services.gateway.flags).toEqual([]);
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* What `gcloud run services describe` shows on a v0.10.35 install — after
|
|
287
|
+
* #579 (Secret Manager references, the gateway on its own SA), before #613
|
|
288
|
+
* (the gateway still on an admin_role pool with admin_role's password).
|
|
289
|
+
*/
|
|
290
|
+
function v01035Install() {
|
|
291
|
+
const url = (role: string, db: string) =>
|
|
292
|
+
`postgresql://${role}@${HOST}:5432/${db}?sslmode=require`;
|
|
293
|
+
const ref = (name: string, secret: string) => ({
|
|
294
|
+
name,
|
|
295
|
+
valueFrom: { secretKeyRef: { name: `caelo-production-${secret}`, key: "latest" } },
|
|
296
|
+
});
|
|
297
|
+
const common = {
|
|
298
|
+
CAELO_PROVIDER: "gcp",
|
|
299
|
+
CAELO_ENV: "production",
|
|
300
|
+
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
301
|
+
};
|
|
302
|
+
const admin = liveContainerEnv(
|
|
303
|
+
serviceJson([
|
|
304
|
+
...plainEnv({
|
|
305
|
+
...common,
|
|
306
|
+
ADMIN_DATABASE_URL: url("admin_role", "cms_admin"),
|
|
307
|
+
PUBLIC_ADMIN_DATABASE_URL: url("admin_role", "cms_public"),
|
|
308
|
+
}),
|
|
309
|
+
ref("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
310
|
+
ref("PUBLIC_ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
311
|
+
ref("CAELO_SECRET_KEK", "secret-kek"),
|
|
312
|
+
ref("CAELO_INTERNAL_SECRET", "internal-secret"),
|
|
313
|
+
ref("CAELO_TOOL_APPROVAL_SECRET", "tool-approval-secret"),
|
|
314
|
+
]),
|
|
315
|
+
);
|
|
316
|
+
const gateway = liveContainerEnv(
|
|
317
|
+
serviceJson([
|
|
318
|
+
...plainEnv({
|
|
319
|
+
...common,
|
|
320
|
+
PUBLIC_DATABASE_URL: url("public_role", "cms_public"),
|
|
321
|
+
ADMIN_DATABASE_URL: url("admin_role", "cms_admin"),
|
|
322
|
+
}),
|
|
323
|
+
ref("ADMIN_DATABASE_PASSWORD", "postgres-password"),
|
|
324
|
+
ref("PUBLIC_DATABASE_PASSWORD", "postgres-password"),
|
|
325
|
+
]),
|
|
326
|
+
);
|
|
327
|
+
return {
|
|
328
|
+
admin: new FakeService(admin, SERVICE_ACCOUNTS.admin),
|
|
329
|
+
gateway: new FakeService(gateway, SERVICE_ACCOUNTS.gateway),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", () => {
|
|
334
|
+
it("drops the admin_role credential and adds the gateway's own logins in one revision", () => {
|
|
335
|
+
const services = v01035Install();
|
|
336
|
+
const plan = upgrade(services);
|
|
337
|
+
expect(services.gateway.revisions).toHaveLength(2);
|
|
338
|
+
expectGatewayOnItsOwnLogins(containerEnv(services.gateway.env));
|
|
339
|
+
// The flags name what changes, never a password.
|
|
340
|
+
expect(JSON.stringify(plan)).not.toContain(PW);
|
|
341
|
+
expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
|
|
342
|
+
"ADMIN_DATABASE_PASSWORD",
|
|
343
|
+
"ADMIN_DATABASE_URL",
|
|
344
|
+
// #650 — the staging gate key arrives in the same roll.
|
|
345
|
+
"CAELO_STAGING_GATE_SECRET",
|
|
346
|
+
"GATEWAY_DATABASE_PASSWORD",
|
|
347
|
+
"GATEWAY_DATABASE_URL",
|
|
348
|
+
"PUBLIC_DATABASE_PASSWORD",
|
|
349
|
+
]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("never leaves a gateway revision without its public pool; the admin keeps its credentials", () => {
|
|
353
|
+
const services = v01035Install();
|
|
354
|
+
const adminBefore = containerEnv(services.admin.env);
|
|
355
|
+
upgrade(services);
|
|
356
|
+
for (const revision of services.gateway.revisions) {
|
|
357
|
+
expect(revision.has("PUBLIC_DATABASE_URL")).toBe(true);
|
|
358
|
+
expect(revision.has("PUBLIC_DATABASE_PASSWORD")).toBe(true);
|
|
359
|
+
}
|
|
360
|
+
const adminAfter = containerEnv(services.admin.env);
|
|
361
|
+
for (const vars of [["ADMIN_DATABASE_URL"], ["PUBLIC_ADMIN_DATABASE_URL"]]) {
|
|
362
|
+
expect(credentials(databaseUrlFromEnv(vars, adminAfter))).toBe(
|
|
363
|
+
credentials(databaseUrlFromEnv(vars, adminBefore)),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("is idempotent: a second upgrade changes no env", () => {
|
|
369
|
+
const services = v01035Install();
|
|
370
|
+
upgrade(services);
|
|
371
|
+
const again = upgrade(services);
|
|
372
|
+
expect(again.ok && again.services.gateway.flags).toEqual([]);
|
|
373
|
+
expect(again.ok && again.services.admin.flags).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("plain → Secret Manager migration of an existing var", () => {
|
|
378
|
+
it("never leaves a revision without the var", () => {
|
|
379
|
+
// An operator set CAELO_INTERNAL_SECRET by hand as a plain value.
|
|
380
|
+
const svc = new FakeService(
|
|
381
|
+
new Map<string, LiveEnvValue>([["CAELO_INTERNAL_SECRET", { kind: "value", value: "hand" }]]),
|
|
382
|
+
SERVICE_ACCOUNTS.admin,
|
|
383
|
+
);
|
|
384
|
+
const plan = planEnvUpdate(svc.env, [
|
|
385
|
+
{
|
|
386
|
+
name: "CAELO_INTERNAL_SECRET",
|
|
387
|
+
valueSource: {
|
|
388
|
+
secretKeyRef: { secret: "caelo-production-internal-secret", version: "latest" },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
392
|
+
if (!plan.ok) throw new Error(plan.error);
|
|
393
|
+
svc.update(plan.flags);
|
|
394
|
+
expect(svc.revisions).toHaveLength(2);
|
|
395
|
+
for (const revision of svc.revisions) expect(revision.has("CAELO_INTERNAL_SECRET")).toBe(true);
|
|
396
|
+
expect(svc.env.get("CAELO_INTERNAL_SECRET")).toEqual({
|
|
397
|
+
kind: "secret",
|
|
398
|
+
secret: "caelo-production-internal-secret",
|
|
399
|
+
version: "latest",
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("the same switch without the in-update removal is what gcloud rejects", () => {
|
|
404
|
+
const svc = new FakeService(
|
|
405
|
+
new Map<string, LiveEnvValue>([["X", { kind: "value", value: "v" }]]),
|
|
406
|
+
SERVICE_ACCOUNTS.admin,
|
|
407
|
+
);
|
|
408
|
+
expect(() => svc.update(["--update-secrets=X=s:latest"])).toThrow(/different|given type/);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("liveEnvHasInlinePassword", () => {
|
|
413
|
+
it("flags the v0.10.29 shape and clears once upgraded", () => {
|
|
414
|
+
const services = v01029Install("postgresql");
|
|
415
|
+
expect(liveEnvHasInlinePassword(services.admin.env)).toBe(true);
|
|
416
|
+
expect(liveEnvHasInlinePassword(services.gateway.env)).toBe(true);
|
|
417
|
+
upgrade(services);
|
|
418
|
+
expect(liveEnvHasInlinePassword(services.admin.env)).toBe(false);
|
|
419
|
+
expect(liveEnvHasInlinePassword(services.gateway.env)).toBe(false);
|
|
420
|
+
});
|
|
421
|
+
});
|