139 lines
5.1 KiB
TypeScript
139 lines
5.1 KiB
TypeScript
|
|
// components/postgres.ts (T03)
|
||
|
|
//
|
||
|
|
// foundation-postgres — the relational core (CONTRACT_003 §3.2): users, orgs, CI,
|
||
|
|
// package metadata for Forgejo. Internal-only (5432 NOT published); reached by
|
||
|
|
// Forgejo over foundation-net by container name. The named volume
|
||
|
|
// foundation-postgres-data holds the cluster (CONTRACT_003 §3.4 — backed up via
|
||
|
|
// pg_dump, CONTRACT_004).
|
||
|
|
//
|
||
|
|
// The superuser password comes from generateCredentials() (CONTRACT_002). The
|
||
|
|
// forgejo login role + database are created post-boot by an idempotent,
|
||
|
|
// readiness-gated remote.Command (docker exec over SSH — ADR-007), since 5432 is
|
||
|
|
// not reachable from the operator running Pulumi.
|
||
|
|
import * as pulumi from "@pulumi/pulumi";
|
||
|
|
import * as docker from "@pulumi/docker";
|
||
|
|
import * as command from "@pulumi/command";
|
||
|
|
import { DeployCtx } from "../lib/context";
|
||
|
|
import { vmConnection } from "../lib/remote";
|
||
|
|
import { PostgresCredentials } from "./credentials";
|
||
|
|
|
||
|
|
export interface PostgresOutputs {
|
||
|
|
container: docker.Container;
|
||
|
|
/** The forgejo role+DB exist once this resolves — gates Forgejo (T08). */
|
||
|
|
ready: command.remote.Command;
|
||
|
|
/** Internal endpoint for app.ini / consumers (CONTRACT_003 §3.3). */
|
||
|
|
endpoint: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Idempotent, readiness-gated role+DB setup (ADR-007). The two SECRET passwords
|
||
|
|
// arrive on stdin (NOT inlined into the command — the command provider echoes the
|
||
|
|
// command string on error, which would leak them; stdin is never echoed — D2).
|
||
|
|
// Non-secret identifiers ($SUPER_USER/$SUPER_DB/$DB_USER/$DB_NAME) are prepended
|
||
|
|
// by the caller. They are fixed, vetted config (lowercase, no specials) so they
|
||
|
|
// expand straight into the SQL; the password is a single-quoted SQL literal,
|
||
|
|
// alphanumeric (special:false) ⇒ injection-safe. (psql does not interpolate
|
||
|
|
// :'var' in -c, so the statement is assembled in the shell.)
|
||
|
|
const ROLE_DB_SETUP = `set -eu
|
||
|
|
IFS= read -r SUPER_PW
|
||
|
|
IFS= read -r DB_PW
|
||
|
|
C=foundation-postgres
|
||
|
|
ready=
|
||
|
|
for _ in $(seq 1 30); do
|
||
|
|
if docker exec -e PGPASSWORD="$SUPER_PW" "$C" pg_isready -U "$SUPER_USER" -d "$SUPER_DB" >/dev/null 2>&1; then
|
||
|
|
ready=1; break
|
||
|
|
fi
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
[ "$ready" = 1 ] || { echo "foundation-postgres not ready after 60s" >&2; exit 1; }
|
||
|
|
|
||
|
|
px() { docker exec -e PGPASSWORD="$SUPER_PW" "$C" psql -qXtA -U "$SUPER_USER" -d "$SUPER_DB" "$@"; }
|
||
|
|
|
||
|
|
if [ "$(px -c "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'")" = 1 ]; then
|
||
|
|
px -c "ALTER ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PW'"
|
||
|
|
else
|
||
|
|
px -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PW'"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$(px -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'")" != 1 ]; then
|
||
|
|
px -c "CREATE DATABASE $DB_NAME OWNER $DB_USER"
|
||
|
|
fi
|
||
|
|
echo "foundation-postgres: role $DB_USER + database $DB_NAME ready"`;
|
||
|
|
|
||
|
|
export function deployPostgres(
|
||
|
|
ctx: DeployCtx,
|
||
|
|
creds: PostgresCredentials,
|
||
|
|
): PostgresOutputs {
|
||
|
|
const { cfg, provider, network } = ctx;
|
||
|
|
|
||
|
|
const image = new docker.RemoteImage(
|
||
|
|
"foundation-postgres-image",
|
||
|
|
{ name: ctx.image("POSTGRES"), keepLocally: true },
|
||
|
|
{ provider },
|
||
|
|
);
|
||
|
|
|
||
|
|
const volume = new docker.Volume(
|
||
|
|
"foundation-postgres-data",
|
||
|
|
{ name: "foundation-postgres-data" },
|
||
|
|
{ provider, retainOnDelete: true }, // never silently drop the cluster
|
||
|
|
);
|
||
|
|
|
||
|
|
const container = new docker.Container(
|
||
|
|
"foundation-postgres",
|
||
|
|
{
|
||
|
|
name: "foundation-postgres",
|
||
|
|
image: image.imageId,
|
||
|
|
hostname: "foundation-postgres",
|
||
|
|
restart: "unless-stopped",
|
||
|
|
envs: [
|
||
|
|
pulumi.interpolate`POSTGRES_USER=${creds.superUser}`,
|
||
|
|
pulumi.interpolate`POSTGRES_PASSWORD=${creds.superPassword}`,
|
||
|
|
`POSTGRES_DB=${cfg.postgres.db}`,
|
||
|
|
],
|
||
|
|
volumes: [
|
||
|
|
{ volumeName: volume.name, containerPath: "/var/lib/postgresql/data" },
|
||
|
|
],
|
||
|
|
networksAdvanced: [
|
||
|
|
{ name: network.name, aliases: ["foundation-postgres"] },
|
||
|
|
],
|
||
|
|
healthcheck: {
|
||
|
|
tests: [
|
||
|
|
"CMD-SHELL",
|
||
|
|
`pg_isready -U ${creds.superUser} -d ${cfg.postgres.db} || exit 1`,
|
||
|
|
],
|
||
|
|
interval: "10s",
|
||
|
|
timeout: "5s",
|
||
|
|
retries: 5,
|
||
|
|
startPeriod: "10s",
|
||
|
|
},
|
||
|
|
logDriver: "json-file",
|
||
|
|
logOpts: { "max-size": "10m", "max-file": "3" },
|
||
|
|
},
|
||
|
|
{ provider, dependsOn: [network], deleteBeforeReplace: true },
|
||
|
|
);
|
||
|
|
|
||
|
|
// Non-secret identifiers prepended to the static script; the two passwords go
|
||
|
|
// on stdin (trailing newline so the final `read` sees EOL, not EOF, under -e).
|
||
|
|
const create = pulumi.interpolate`SUPER_USER='${creds.superUser}'
|
||
|
|
SUPER_DB='${cfg.postgres.db}'
|
||
|
|
DB_USER='${creds.forgejoDbUser}'
|
||
|
|
DB_NAME='${cfg.postgres.forgejoDb}'
|
||
|
|
${ROLE_DB_SETUP}`;
|
||
|
|
|
||
|
|
const ready = new command.remote.Command(
|
||
|
|
"foundation-postgres-roledb",
|
||
|
|
{
|
||
|
|
connection: vmConnection(ctx),
|
||
|
|
create,
|
||
|
|
stdin: pulumi.interpolate`${creds.superPassword}\n${creds.forgejoDbPassword}\n`,
|
||
|
|
// The VM's sshd rejects setenv (no AcceptEnv); don't try to export the prior
|
||
|
|
// output into the remote env (avoids a noisy warning on every run).
|
||
|
|
addPreviousOutputInEnv: false,
|
||
|
|
// Re-run when the container is replaced or the forgejo password rotates.
|
||
|
|
triggers: [container.id, creds.forgejoDbPassword],
|
||
|
|
},
|
||
|
|
{ dependsOn: [container] },
|
||
|
|
);
|
||
|
|
|
||
|
|
return { container, ready, endpoint: "foundation-postgres:5432" };
|
||
|
|
}
|