@caelo-cms/provisioning 0.11.10 → 0.11.12
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 +196 -23
- 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 +12 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +42 -3
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +34 -2
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +76 -1
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job-setup.d.ts.map +1 -1
- package/dist/converge-job-setup.js +4 -1
- package/dist/converge-job-setup.js.map +1 -1
- package/dist/converge-job.d.ts +44 -3
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +105 -11
- 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 +10 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +10 -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/mcp-auth-proxy-setup.d.ts +3 -1
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
- package/dist/mcp-auth-proxy-setup.js +9 -4
- package/dist/mcp-auth-proxy-setup.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/upgrader-iam-preflight.d.ts +8 -6
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +5 -1
- package/dist/upgrader-iam-preflight.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 +82 -12
- 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 +42 -2
- package/src/converge-job-preflight.test.ts +106 -0
- package/src/converge-job-preflight.ts +97 -3
- package/src/converge-job-setup.ts +4 -1
- package/src/converge-job.test.ts +213 -0
- package/src/converge-job.ts +175 -19
- 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 +10 -1
- package/src/install-state.ts +7 -0
- package/src/lifecycle.ts +3 -0
- package/src/mcp-auth-proxy-setup.test.ts +33 -1
- package/src/mcp-auth-proxy-setup.ts +11 -4
- 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/upgrader-iam-preflight.ts +15 -10
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +54 -1
- package/src/wizards/gcp-cost.ts +102 -12
- 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
package/src/self-hosted-roles.ts
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* as the Postgres superuser BEFORE it writes the new Compose file: it
|
|
14
14
|
* creates missing roles and databases and sets each role's password to the
|
|
15
15
|
* configured one. It also drops the former `gateway_role` (#635: the
|
|
16
|
-
* gateway has no cms_admin login), with whatever it still holds
|
|
17
|
-
*
|
|
16
|
+
* gateway has no cms_admin login), with whatever it still holds, and
|
|
17
|
+
* (#1329) creates the staging database `cms_public_staging` with the
|
|
18
|
+
* staging gateway's login `public_staging_role` on an install from before
|
|
19
|
+
* it. Every statement is idempotent.
|
|
18
20
|
*
|
|
19
21
|
* Passwords travel on psql's stdin, never on argv.
|
|
20
22
|
*/
|
|
@@ -35,7 +37,10 @@ function literal(s: string): string {
|
|
|
35
37
|
export const AGENT_DB_ROLE = "caelo_agent";
|
|
36
38
|
|
|
37
39
|
/** The application roles bootstrap.sh and the convergence create. */
|
|
38
|
-
const APP_ROLES = ["admin_role", "public_role"] as const;
|
|
40
|
+
const APP_ROLES = ["admin_role", "public_role", "public_staging_role"] as const;
|
|
41
|
+
|
|
42
|
+
/** #1329 — the gateway logins that carry {@link PUBLIC_ROLE_LIMITS}: the live and the staging one. */
|
|
43
|
+
const GATEWAY_LOGINS = ["public_role", "public_staging_role"] as const;
|
|
39
44
|
type AppRole = (typeof APP_ROLES)[number];
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -47,7 +52,8 @@ type AppRole = (typeof APP_ROLES)[number];
|
|
|
47
52
|
* (packages/migrations role-audit.ts) refuses an install without them.
|
|
48
53
|
* Self-hosted admin_role has no CREATEROLE, so the superuser applies them
|
|
49
54
|
* here — on the volume's first start (bootstrap.sh), on every `up`, and
|
|
50
|
-
* before the migrations of every upgrade ({@link
|
|
55
|
+
* before the migrations of every upgrade ({@link preMigrationRolesScript}).
|
|
56
|
+
* Since #1329 the staging gateway's public_staging_role carries the same.
|
|
51
57
|
*/
|
|
52
58
|
export const PUBLIC_ROLE_LIMITS = {
|
|
53
59
|
connectionLimit: 30,
|
|
@@ -56,31 +62,57 @@ export const PUBLIC_ROLE_LIMITS = {
|
|
|
56
62
|
workMem: "1MB",
|
|
57
63
|
} as const;
|
|
58
64
|
|
|
59
|
-
/** The statements that apply {@link PUBLIC_ROLE_LIMITS} (idempotent). */
|
|
65
|
+
/** The statements that apply {@link PUBLIC_ROLE_LIMITS} to both gateway logins (idempotent). */
|
|
60
66
|
function publicRoleLimitsSql(): string[] {
|
|
61
67
|
const l = PUBLIC_ROLE_LIMITS;
|
|
68
|
+
return GATEWAY_LOGINS.flatMap((role) => [
|
|
69
|
+
`ALTER ROLE ${role} CONNECTION LIMIT ${l.connectionLimit};`,
|
|
70
|
+
`ALTER ROLE ${role} SET statement_timeout = '${l.statementTimeout}';`,
|
|
71
|
+
`ALTER ROLE ${role} SET idle_in_transaction_session_timeout = '${l.idleInTransactionSessionTimeout}';`,
|
|
72
|
+
`ALTER ROLE ${role} SET work_mem = '${l.workMem}';`,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** How a role's password appears in the SQL: a literal, or a psql variable. */
|
|
77
|
+
type PasswordSql = (role: AppRole | typeof AGENT_DB_ROLE) => string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* #1329 — the staging database and its gateway login, with each gateway
|
|
81
|
+
* login on its own database only (the same as bootstrap.sh and cms_admin
|
|
82
|
+
* migration 0530). Idempotent: an install from before #1329 gets both, an
|
|
83
|
+
* install that has them gets the password re-applied.
|
|
84
|
+
*/
|
|
85
|
+
function stagingDatabaseSql(password: PasswordSql): string[] {
|
|
62
86
|
return [
|
|
63
|
-
|
|
64
|
-
`ALTER ROLE
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
"SELECT 'CREATE ROLE public_staging_role NOINHERIT LOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'public_staging_role') \\gexec",
|
|
88
|
+
`ALTER ROLE public_staging_role WITH LOGIN PASSWORD ${password("public_staging_role")};`,
|
|
89
|
+
"SELECT 'CREATE DATABASE cms_public_staging OWNER admin_role' WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'cms_public_staging') \\gexec",
|
|
90
|
+
"REVOKE ALL ON DATABASE cms_public_staging FROM PUBLIC;",
|
|
91
|
+
"GRANT CONNECT ON DATABASE cms_public_staging TO public_staging_role, admin_role;",
|
|
92
|
+
"REVOKE ALL ON DATABASE cms_public_staging FROM public_role;",
|
|
93
|
+
"REVOKE ALL ON DATABASE cms_public FROM public_staging_role;",
|
|
94
|
+
"REVOKE ALL ON DATABASE cms_admin FROM public_staging_role;",
|
|
67
95
|
];
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
/**
|
|
71
|
-
* The psql script (superuser)
|
|
72
|
-
*
|
|
73
|
-
*
|
|
99
|
+
* The psql script (superuser) the self-hosted upgrade runs BEFORE the
|
|
100
|
+
* migrations: the gateway logins' {@link PUBLIC_ROLE_LIMITS} (#1338) and
|
|
101
|
+
* (#1329) the staging database with its login. The migrations' role audit
|
|
102
|
+
* refuses either gateway login without its limits, and the
|
|
103
|
+
* cms_public_staging run needs the database; the full convergence must
|
|
74
104
|
* wait until after them (it drops roles a migration retired). Fails when
|
|
75
105
|
* public_role does not exist — the install's roles were never created.
|
|
76
106
|
*/
|
|
77
|
-
export function
|
|
78
|
-
return [
|
|
107
|
+
export function preMigrationRolesScript(stagingPassword: string): string {
|
|
108
|
+
return [
|
|
109
|
+
"\\set ON_ERROR_STOP on",
|
|
110
|
+
...stagingDatabaseSql(() => literal(stagingPassword)),
|
|
111
|
+
...publicRoleLimitsSql(),
|
|
112
|
+
"",
|
|
113
|
+
].join("\n");
|
|
79
114
|
}
|
|
80
115
|
|
|
81
|
-
/** How a role's password appears in the SQL: a literal, or a psql variable. */
|
|
82
|
-
type PasswordSql = (role: AppRole | typeof AGENT_DB_ROLE) => string;
|
|
83
|
-
|
|
84
116
|
/**
|
|
85
117
|
* The convergence SQL (psql, as the superuser, connected to any database).
|
|
86
118
|
* `password` renders each role's password; `agent` adds the agent's login.
|
|
@@ -111,11 +143,15 @@ function convergenceSql(password: PasswordSql, agent: boolean): string {
|
|
|
111
143
|
"REVOKE ALL ON DATABASE cms_public FROM PUBLIC;",
|
|
112
144
|
"GRANT CONNECT ON DATABASE cms_admin TO admin_role;",
|
|
113
145
|
"GRANT CONNECT ON DATABASE cms_public TO public_role, admin_role;",
|
|
146
|
+
// #1329 — after the other two databases exist: its revokes name them.
|
|
147
|
+
...stagingDatabaseSql(password),
|
|
114
148
|
// #635 — the gateway's former cms_admin login goes, with whatever it
|
|
115
149
|
// still holds in either database (the superuser may drop what
|
|
116
150
|
// migration 0285 could only revoke).
|
|
117
151
|
"\\connect cms_public",
|
|
118
152
|
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP OWNED BY gateway_role; END IF; END $$;",
|
|
153
|
+
"\\connect cms_public_staging",
|
|
154
|
+
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP OWNED BY gateway_role; END IF; END $$;",
|
|
119
155
|
"\\connect cms_admin",
|
|
120
156
|
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP OWNED BY gateway_role; END IF; END $$;",
|
|
121
157
|
"DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gateway_role') THEN DROP ROLE gateway_role; END IF; END $$;",
|
|
@@ -136,6 +172,7 @@ export function roleConvergenceScript(passwords: RolePasswords, agentPassword?:
|
|
|
136
172
|
const byRole: Record<AppRole, string> = {
|
|
137
173
|
admin_role: passwords.admin,
|
|
138
174
|
public_role: passwords.public,
|
|
175
|
+
public_staging_role: passwords.publicStaging,
|
|
139
176
|
};
|
|
140
177
|
return convergenceSql(
|
|
141
178
|
(role) => literal(role === AGENT_DB_ROLE ? (agentPassword ?? "") : byRole[role]),
|
|
@@ -147,6 +184,7 @@ export function roleConvergenceScript(passwords: RolePasswords, agentPassword?:
|
|
|
147
184
|
const PASSWORD_ENV: Record<AppRole | typeof AGENT_DB_ROLE, string> = {
|
|
148
185
|
admin_role: "ADMIN_ROLE_PASSWORD",
|
|
149
186
|
public_role: "PUBLIC_ROLE_PASSWORD",
|
|
187
|
+
public_staging_role: "PUBLIC_STAGING_ROLE_PASSWORD",
|
|
150
188
|
[AGENT_DB_ROLE]: "AGENT_ROLE_PASSWORD",
|
|
151
189
|
};
|
|
152
190
|
|
|
@@ -185,18 +223,20 @@ ${convergenceSql((role) => `:'${role}_pw'`, opts.agent)}CAELO_SQL
|
|
|
185
223
|
export type SuperuserPsql = (script: string) => Promise<{ exitCode: number; stderr: string }>;
|
|
186
224
|
|
|
187
225
|
/**
|
|
188
|
-
* Apply {@link
|
|
226
|
+
* Apply {@link preMigrationRolesScript} through the running Postgres
|
|
189
227
|
* container. Unlike {@link convergeSelfHostedRoles} a stopped database is a
|
|
190
228
|
* failure: the upgrade runs it right before migrations that need it.
|
|
191
229
|
*/
|
|
192
|
-
export async function
|
|
230
|
+
export async function applyPreMigrationRoles(deps: {
|
|
193
231
|
readonly probe: () => Promise<boolean>;
|
|
194
232
|
readonly psql: SuperuserPsql;
|
|
233
|
+
/** #1329 — public_staging_role's password from `.caelo/config.json`. */
|
|
234
|
+
readonly stagingPassword: string;
|
|
195
235
|
}): Promise<ConvergeOutcome> {
|
|
196
236
|
if (!(await deps.probe())) {
|
|
197
237
|
return { ok: false, reachable: false, error: "the Postgres container is not running" };
|
|
198
238
|
}
|
|
199
|
-
const r = await deps.psql(
|
|
239
|
+
const r = await deps.psql(preMigrationRolesScript(deps.stagingPassword));
|
|
200
240
|
if (r.exitCode === 0) return { ok: true };
|
|
201
241
|
return { ok: false, reachable: true, error: r.stderr.trim() || `psql exited ${r.exitCode}` };
|
|
202
242
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1140 — the self-hosted Pulumi stack and the CLI render the same Compose
|
|
5
|
+
* file, so the scheduler interval `init` recorded survives a `pulumi up`,
|
|
6
|
+
* and a disagreeing Pulumi key is refused instead of winning silently.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterAll, describe, expect, it } from "bun:test";
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
recordedSchedulerIntervalMinutes,
|
|
15
|
+
selfHostedSchedulerIntervalMinutes,
|
|
16
|
+
} from "./self-hosted-scheduler-interval.js";
|
|
17
|
+
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "caelo-scheduler-interval-"));
|
|
19
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
20
|
+
|
|
21
|
+
function config(name: string, body: string): string {
|
|
22
|
+
const path = join(dir, name);
|
|
23
|
+
writeFileSync(path, body);
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("recordedSchedulerIntervalMinutes", () => {
|
|
28
|
+
it("reads what init recorded", () => {
|
|
29
|
+
expect(
|
|
30
|
+
recordedSchedulerIntervalMinutes(config("a.json", '{"schedulerIntervalMinutes":2}')),
|
|
31
|
+
).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is undefined without a config.json or without the key", () => {
|
|
35
|
+
expect(recordedSchedulerIntervalMinutes(join(dir, "missing.json"))).toBeUndefined();
|
|
36
|
+
expect(recordedSchedulerIntervalMinutes(config("b.json", '{"domain":"x"}'))).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("refuses a broken file or an out-of-range value", () => {
|
|
40
|
+
expect(() => recordedSchedulerIntervalMinutes(config("c.json", "{"))).toThrow("not valid JSON");
|
|
41
|
+
expect(() =>
|
|
42
|
+
recordedSchedulerIntervalMinutes(config("d.json", '{"schedulerIntervalMinutes":30}')),
|
|
43
|
+
).toThrow("between 1 and 15");
|
|
44
|
+
expect(() =>
|
|
45
|
+
recordedSchedulerIntervalMinutes(config("e.json", '{"schedulerIntervalMinutes":"2"}')),
|
|
46
|
+
).toThrow("not a number");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("selfHostedSchedulerIntervalMinutes", () => {
|
|
51
|
+
it("keeps the recorded interval when the Pulumi key is unset", () => {
|
|
52
|
+
expect(selfHostedSchedulerIntervalMinutes(undefined, 2, "config.json")).toBe(2);
|
|
53
|
+
expect(selfHostedSchedulerIntervalMinutes(undefined, undefined, "config.json")).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("takes the Pulumi key on a Pulumi-only install, or when both agree", () => {
|
|
57
|
+
expect(selfHostedSchedulerIntervalMinutes(3, undefined, "config.json")).toBe(3);
|
|
58
|
+
expect(selfHostedSchedulerIntervalMinutes(2, 2, "config.json")).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("refuses a Pulumi key that disagrees with the record", () => {
|
|
62
|
+
expect(() => selfHostedSchedulerIntervalMinutes(5, 2, "config.json")).toThrow(
|
|
63
|
+
"config.json records 2",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* #1140 — the self-hosted scheduler interval has two places it can be set:
|
|
5
|
+
* `cms-provision init --scheduler-interval-minutes` records it in
|
|
6
|
+
* `.caelo/config.json` (which every CLI render — `up`, `upgrade`, agent
|
|
7
|
+
* apply — reads), and the Pulumi stack has the config key
|
|
8
|
+
* `caelo-self-hosted:schedulerIntervalMinutes`. Both rewrite the same
|
|
9
|
+
* `docker-compose.yml`, so if each read only its own source a `pulumi up`
|
|
10
|
+
* over a CLI install would silently put the interval back to the default,
|
|
11
|
+
* and the next CLI `upgrade` would undo a Pulumi-only value the same way.
|
|
12
|
+
*
|
|
13
|
+
* The recorded `.caelo/config.json` is the install's one record (the CLI
|
|
14
|
+
* renders from nothing else), so the stack takes the interval from it; the
|
|
15
|
+
* Pulumi key may restate it but a different value is refused rather than
|
|
16
|
+
* one side winning silently (CLAUDE.md §2: no fallbacks).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { parseSchedulerIntervalMinutes } from "./stack-contract.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the interval `init` recorded in a `.caelo/config.json`.
|
|
24
|
+
*
|
|
25
|
+
* @param configPath - the install's `config.json`.
|
|
26
|
+
* @returns the recorded minutes, or undefined when there is no config.json
|
|
27
|
+
* (a Pulumi-only install) or it records no interval (the default).
|
|
28
|
+
* @throws when the file is not JSON or the recorded value is out of bounds.
|
|
29
|
+
*/
|
|
30
|
+
export function recordedSchedulerIntervalMinutes(configPath: string): number | undefined {
|
|
31
|
+
if (!existsSync(configPath)) return undefined;
|
|
32
|
+
let parsed: unknown;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw new Error(`${configPath} is not valid JSON: ${(e as Error).message}`);
|
|
37
|
+
}
|
|
38
|
+
const raw =
|
|
39
|
+
parsed !== null && typeof parsed === "object"
|
|
40
|
+
? (parsed as { schedulerIntervalMinutes?: unknown }).schedulerIntervalMinutes
|
|
41
|
+
: undefined;
|
|
42
|
+
if (raw === undefined) return undefined;
|
|
43
|
+
if (typeof raw !== "number") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`${configPath}: schedulerIntervalMinutes ${JSON.stringify(raw)} is not a number of minutes`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return parseSchedulerIntervalMinutes(raw, `${configPath}: schedulerIntervalMinutes`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The interval the self-hosted Pulumi stack renders into the Compose file.
|
|
53
|
+
*
|
|
54
|
+
* @param pulumiValue - `caelo-self-hosted:schedulerIntervalMinutes`, if set.
|
|
55
|
+
* @param recorded - what {@link recordedSchedulerIntervalMinutes} read.
|
|
56
|
+
* @param configPath - named in the refusal.
|
|
57
|
+
* @returns the minutes to render; undefined means neither source sets one
|
|
58
|
+
* (the generator's default).
|
|
59
|
+
* @throws when both are set and differ.
|
|
60
|
+
*/
|
|
61
|
+
export function selfHostedSchedulerIntervalMinutes(
|
|
62
|
+
pulumiValue: number | undefined,
|
|
63
|
+
recorded: number | undefined,
|
|
64
|
+
configPath: string,
|
|
65
|
+
): number | undefined {
|
|
66
|
+
if (pulumiValue === undefined) return recorded;
|
|
67
|
+
const minutes = parseSchedulerIntervalMinutes(
|
|
68
|
+
pulumiValue,
|
|
69
|
+
"caelo-self-hosted:schedulerIntervalMinutes",
|
|
70
|
+
);
|
|
71
|
+
if (recorded !== undefined && recorded !== minutes) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`caelo-self-hosted:schedulerIntervalMinutes is ${minutes} but ${configPath} records ${recorded}; ` +
|
|
74
|
+
"the CLI renders the Compose file from config.json, so the two must agree. " +
|
|
75
|
+
`Set the Pulumi key to ${recorded}, or remove it (\`pulumi config rm caelo-self-hosted:schedulerIntervalMinutes\`) to use the recorded value.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return minutes;
|
|
79
|
+
}
|
|
@@ -36,6 +36,7 @@ describe("startSelfHosted (#707 — one command ends with a serving install)", (
|
|
|
36
36
|
"up -d --wait postgres",
|
|
37
37
|
`${MIGRATE} admin`,
|
|
38
38
|
`${MIGRATE} public`,
|
|
39
|
+
`${MIGRATE} public-staging`,
|
|
39
40
|
"up -d --remove-orphans",
|
|
40
41
|
"config --services",
|
|
41
42
|
"exec -T caelo-admin bun -e <probe>",
|
|
@@ -151,7 +151,7 @@ function deps(
|
|
|
151
151
|
runner.calls.push("converge roles");
|
|
152
152
|
return roles;
|
|
153
153
|
},
|
|
154
|
-
|
|
154
|
+
applyPreMigrationRoles: async () => {
|
|
155
155
|
runner.calls.push("public role limits");
|
|
156
156
|
return { ok: true } as const;
|
|
157
157
|
},
|
|
@@ -187,6 +187,7 @@ describe("upgradeSelfHosted", () => {
|
|
|
187
187
|
"public role limits",
|
|
188
188
|
"migrate admin",
|
|
189
189
|
"migrate public",
|
|
190
|
+
"migrate public-staging",
|
|
190
191
|
"converge roles",
|
|
191
192
|
"up caelo-admin",
|
|
192
193
|
"health admin",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* 3b. #1338 — recreate Postgres when the Compose file changed it (TLS with
|
|
13
13
|
* the install's own certificate, self-hosted-db-tls.ts: the new images
|
|
14
14
|
* connect with verify-full only; the old containers keep connecting,
|
|
15
|
-
* Postgres still accepts them) and
|
|
15
|
+
* Postgres still accepts them) and, as the superuser, create the
|
|
16
|
+
* staging database with its gateway login (#1329) and apply both gateway
|
|
17
|
+
* logins' limits as the
|
|
16
18
|
* superuser (the migrations' role audit refuses public_role without
|
|
17
19
|
* them, and the self-hosted admin_role cannot set them);
|
|
18
20
|
* 4. run the migrations from the NEW admin image (`docker compose run`,
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
MEDIA_DIR,
|
|
66
68
|
type ReleaseImages,
|
|
67
69
|
} from "./compose.js";
|
|
70
|
+
import { MIGRATION_TARGETS } from "./migration-targets.js";
|
|
68
71
|
import { selfCommand } from "./self-command.js";
|
|
69
72
|
import {
|
|
70
73
|
type HostRunner,
|
|
@@ -125,11 +128,13 @@ export interface SelfHostedUpgradeDeps {
|
|
|
125
128
|
*/
|
|
126
129
|
readonly convergeRoles: () => Promise<ConvergeOutcome>;
|
|
127
130
|
/**
|
|
128
|
-
* #1338
|
|
129
|
-
*
|
|
131
|
+
* #1338 / #1329 — as the Postgres superuser (`applyPreMigrationRoles`):
|
|
132
|
+
* creates the staging database and its gateway login on an install from
|
|
133
|
+
* before them, and applies both gateway logins' limits, before the
|
|
134
|
+
* migrations that run on that database and whose role audit
|
|
130
135
|
* checks them.
|
|
131
136
|
*/
|
|
132
|
-
readonly
|
|
137
|
+
readonly applyPreMigrationRoles: () => Promise<ConvergeOutcome>;
|
|
133
138
|
/**
|
|
134
139
|
* Re-writes the Caddyfile from the install's config + domains table and
|
|
135
140
|
* reloads Caddy (#891): the edge headers (the SVG CSP, nosniff) live in
|
|
@@ -401,12 +406,12 @@ export async function upgradeSelfHosted(
|
|
|
401
406
|
back.exitCode === 0,
|
|
402
407
|
);
|
|
403
408
|
}
|
|
404
|
-
const limits = await deps.
|
|
409
|
+
const limits = await deps.applyPreMigrationRoles();
|
|
405
410
|
if (!limits.ok) {
|
|
406
411
|
restoreCompose(file, previousCompose);
|
|
407
412
|
return fail(
|
|
408
413
|
"roles",
|
|
409
|
-
`Could not
|
|
414
|
+
`Could not prepare the database roles for the migrations (the staging database cms_public_staging and the gateway logins' connection and statement limits): ${limits.error}\nThe previous release keeps running; no migration ran. Database backup: ${backup.dir}`,
|
|
410
415
|
true,
|
|
411
416
|
);
|
|
412
417
|
}
|
|
@@ -600,7 +605,8 @@ export async function dumpDatabases(
|
|
|
600
605
|
}
|
|
601
606
|
|
|
602
607
|
/**
|
|
603
|
-
* Apply the committed migrations (cms_admin, then cms_public
|
|
608
|
+
* Apply the committed migrations (cms_admin, then cms_public and its staging
|
|
609
|
+
* twin cms_public_staging, #1329) with the
|
|
604
610
|
* admin image the Compose file names, as a one-off container — the
|
|
605
611
|
* self-hosted equivalent of the GCP `caelo-migrate-<target>` Cloud Run job.
|
|
606
612
|
* drizzle's bookkeeping makes re-runs apply only new migrations.
|
|
@@ -609,7 +615,7 @@ export async function runMigrations(
|
|
|
609
615
|
caeloDir: string,
|
|
610
616
|
runner: HostRunner,
|
|
611
617
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
612
|
-
for (const target of
|
|
618
|
+
for (const target of MIGRATION_TARGETS) {
|
|
613
619
|
const r = await runner.run(
|
|
614
620
|
compose(
|
|
615
621
|
caeloDir,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
RETIRED_SECRET_FILES,
|
|
24
24
|
RETIRED_SERVICE_ENV,
|
|
25
25
|
runtimeSecretBindings,
|
|
26
|
+
SECRETS_SET_UP_AHEAD_OF_THEIR_READER,
|
|
26
27
|
SERVICE_SECRET_ENV,
|
|
27
28
|
SERVICE_SECRET_FILES,
|
|
28
29
|
SERVICE_VPC_EGRESS,
|
|
@@ -52,6 +53,7 @@ const asRecord = (vars: CloudRunEnvVar[]) =>
|
|
|
52
53
|
const ADMIN_SECRETS = {
|
|
53
54
|
ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
54
55
|
PUBLIC_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
56
|
+
PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
55
57
|
CAELO_SECRET_KEK: "secret:caelo-production-secret-kek:latest",
|
|
56
58
|
CAELO_INTERNAL_SECRET: "secret:caelo-production-internal-secret:latest",
|
|
57
59
|
CAELO_TOOL_APPROVAL_SECRET: "secret:caelo-production-tool-approval-secret:latest",
|
|
@@ -67,6 +69,10 @@ describe("databaseUrls", () => {
|
|
|
67
69
|
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
68
70
|
public:
|
|
69
71
|
"postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
72
|
+
publicStagingAdmin:
|
|
73
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
74
|
+
publicStaging:
|
|
75
|
+
"postgresql://public_staging_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
70
76
|
});
|
|
71
77
|
});
|
|
72
78
|
});
|
|
@@ -81,11 +87,14 @@ describe("adminEnvContract", () => {
|
|
|
81
87
|
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
82
88
|
PUBLIC_ADMIN_DATABASE_URL:
|
|
83
89
|
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
90
|
+
PUBLIC_STAGING_ADMIN_DATABASE_URL:
|
|
91
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
84
92
|
CAELO_SITE_URL: "https://acme.com",
|
|
85
93
|
CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
|
|
86
94
|
MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
|
|
87
95
|
CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
88
96
|
CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
|
|
97
|
+
CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
89
98
|
// #806 — where Settings → MCP → Remote writes the OAuth client.
|
|
90
99
|
CAELO_OPERATOR_OAUTH_CLIENT_STORE:
|
|
91
100
|
"projects/acme/secrets/caelo-production-operator-oauth-client",
|
|
@@ -121,11 +130,14 @@ describe("adminEnvContract", () => {
|
|
|
121
130
|
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
122
131
|
PUBLIC_ADMIN_DATABASE_URL:
|
|
123
132
|
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
133
|
+
PUBLIC_STAGING_ADMIN_DATABASE_URL:
|
|
134
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
124
135
|
CAELO_SITE_URL: "https://acme.com",
|
|
125
136
|
CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
|
|
126
137
|
MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
|
|
127
138
|
CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
128
139
|
CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
|
|
140
|
+
CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
129
141
|
// #806 — where Settings → MCP → Remote writes the OAuth client.
|
|
130
142
|
CAELO_OPERATOR_OAUTH_CLIENT_STORE:
|
|
131
143
|
"projects/acme/secrets/caelo-production-operator-oauth-client",
|
|
@@ -191,6 +203,8 @@ describe("gatewayEnvContract", () => {
|
|
|
191
203
|
CAELO_PROVIDER: "gcp",
|
|
192
204
|
CAELO_ENV: "production",
|
|
193
205
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
206
|
+
// #1329 — the live site's gateway.
|
|
207
|
+
CAELO_GATEWAY_ENVIRONMENT: "live",
|
|
194
208
|
// #845 — gcp: the LB appends <client>,<LB IP>.
|
|
195
209
|
CAELO_TRUSTED_PROXY_HOPS: "2",
|
|
196
210
|
PUBLIC_DATABASE_URL:
|
|
@@ -267,6 +281,7 @@ describe("database role secrets (#613)", () => {
|
|
|
267
281
|
expect(DATABASE_ROLE_SECRET).toEqual({
|
|
268
282
|
admin_role: "postgres-password",
|
|
269
283
|
public_role: "public-role-password",
|
|
284
|
+
public_staging_role: "public-staging-role-password",
|
|
270
285
|
});
|
|
271
286
|
});
|
|
272
287
|
|
|
@@ -341,11 +356,24 @@ describe("runtime secrets", () => {
|
|
|
341
356
|
]);
|
|
342
357
|
});
|
|
343
358
|
|
|
344
|
-
it("every CLI-generated secret is read by some service", () => {
|
|
359
|
+
it("every CLI-generated secret is read by some service, or says what reads it until one does", () => {
|
|
360
|
+
const read = CLOUD_RUN_SLUGS.flatMap((slug) => serviceSecrets(slug));
|
|
345
361
|
for (const s of CLI_GENERATED_SECRETS) {
|
|
346
|
-
|
|
362
|
+
if (SECRETS_SET_UP_AHEAD_OF_THEIR_READER[s]) {
|
|
363
|
+
// Set up ahead: no service may read it yet, or the entry is stale.
|
|
364
|
+
expect(read).not.toContain(s);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
expect(read).toContain(s);
|
|
347
368
|
}
|
|
348
369
|
});
|
|
370
|
+
|
|
371
|
+
it("the two gateway logins share no password secret, and no service reads the staging one yet (#1329)", () => {
|
|
372
|
+
expect(DATABASE_ROLE_SECRET.public_staging_role).not.toBe(DATABASE_ROLE_SECRET.public_role);
|
|
373
|
+
expect(CLI_GENERATED_SECRETS).toContain("public-staging-role-password");
|
|
374
|
+
expect(serviceSecrets("gateway")).not.toContain("public-staging-role-password");
|
|
375
|
+
expect(serviceSecrets("admin")).not.toContain("public-staging-role-password");
|
|
376
|
+
});
|
|
349
377
|
});
|
|
350
378
|
|
|
351
379
|
describe("self-hosted compose", () => {
|
|
@@ -353,7 +381,7 @@ describe("self-hosted compose", () => {
|
|
|
353
381
|
const yaml = generateDockerCompose({
|
|
354
382
|
domain: "acme.com",
|
|
355
383
|
postgresPassword: "p",
|
|
356
|
-
rolePasswords: { admin: "a", public: "b" },
|
|
384
|
+
rolePasswords: { admin: "a", public: "b", publicStaging: "ps" },
|
|
357
385
|
caeloSecretKek: "k",
|
|
358
386
|
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
359
387
|
internalSecret: "i".repeat(48),
|