// 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" }; }