// components/forgejo.ts (T08) // // foundation-forgejo — the forge (CONTRACT_003 §3.2): git repos on the /data volume // (the irreducible state), metadata in external Postgres, blobs in RustFS S3. // Config is seeded via FORGEJO__section__KEY env (the image's environment-to-ini // writes app.ini on start). INSTALL_LOCK skips the web installer; crypto secrets // (SECRET_KEY/INTERNAL_TOKEN/JWT) are auto-generated by Forgejo on first start and // persist in the volume. HTTP 3000 is internal (Caddy fronts forge.olsitec.net); // the built-in SSH server (container :22) is published on BOTH host :22 (so the // literal goal `git@git.olsitec.net:olsitec/...` works) and :2222 (CONTRACT_003). 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, RustfsCredentials } from "./credentials"; import { PostgresOutputs } from "./postgres"; import { RustfsOutputs } from "./rustfs"; export interface ForgejoDeps { postgres: PostgresOutputs; rustfs: RustfsOutputs; pgCreds: PostgresCredentials; rustfsCreds: RustfsCredentials; } export interface ForgejoOutputs { container: docker.Container; /** Forgejo /api/healthz passes once this resolves — GATE B (T09/T10). */ ready: command.remote.Command; endpoint: string; } export function deployForgejo( ctx: DeployCtx, deps: ForgejoDeps, ): ForgejoOutputs { const { cfg, provider, network } = ctx; const image = new docker.RemoteImage( "foundation-forgejo-image", { name: ctx.image("FORGEJO"), keepLocally: true }, { provider }, ); const volume = new docker.Volume( "foundation-forgejo-data", { name: "foundation-forgejo-data" }, { provider, retainOnDelete: true }, // git repos — the irreplaceable core ); // A RustFS (minio-compatible) storage block for a given Forgejo config section. const minio = ( section: string, bucket: string, ): pulumi.Input[] => [ `FORGEJO__${section}__STORAGE_TYPE=minio`, `FORGEJO__${section}__MINIO_ENDPOINT=foundation-rustfs:9000`, pulumi.interpolate`FORGEJO__${section}__MINIO_ACCESS_KEY_ID=${deps.rustfsCreds.serviceKeyId}`, pulumi.interpolate`FORGEJO__${section}__MINIO_SECRET_ACCESS_KEY=${deps.rustfsCreds.serviceKeySecret}`, `FORGEJO__${section}__MINIO_BUCKET=${bucket}`, `FORGEJO__${section}__MINIO_USE_SSL=false`, `FORGEJO__${section}__MINIO_LOCATION=us-east-1`, ]; const envs: pulumi.Input[] = [ "USER_UID=1000", "USER_GID=1000", // --- database: external Postgres (CONTRACT_003 §3.3) --- "FORGEJO__database__DB_TYPE=postgres", "FORGEJO__database__HOST=foundation-postgres:5432", `FORGEJO__database__NAME=${cfg.postgres.forgejoDb}`, pulumi.interpolate`FORGEJO__database__USER=${deps.pgCreds.forgejoDbUser}`, pulumi.interpolate`FORGEJO__database__PASSWD=${deps.pgCreds.forgejoDbPassword}`, "FORGEJO__database__SSL_MODE=disable", // --- server / web / ssh --- "FORGEJO__server__PROTOCOL=http", `FORGEJO__server__DOMAIN=${cfg.hosts.forge}`, `FORGEJO__server__ROOT_URL=https://${cfg.hosts.forge}/`, "FORGEJO__server__HTTP_ADDR=0.0.0.0", "FORGEJO__server__HTTP_PORT=3000", // Use the IMAGE's openssh sshd on container :22. START_SSH_SERVER MUST be set // false explicitly (not just omitted): env-to-ini only overwrites keys it is // given, so a stale true in the persisted app.ini would keep Forgejo's built-in // Go SSH server colliding on :22. SSH_PORT is the clone-URL port; the sshd is // published on host :22 (scp-form goal) + :2222 (CONTRACT_003). "FORGEJO__server__START_SSH_SERVER=false", "FORGEJO__server__SSH_LISTEN_PORT=22", `FORGEJO__server__SSH_PORT=${cfg.forgeSshPort}`, `FORGEJO__server__SSH_DOMAIN=${cfg.hosts.git}`, "FORGEJO__server__LFS_START_SERVER=true", // --- blobs on RustFS: default storage + LFS (CONTRACT_003 §3.4) --- ...minio("storage", "forgejo-packages"), ...minio("lfs", "forgejo-lfs"), // --- lock the installer; no open registration (admin creates users, T09) --- "FORGEJO__security__INSTALL_LOCK=true", "FORGEJO__service__DISABLE_REGISTRATION=true", "FORGEJO__service__REQUIRE_SIGNIN_VIEW=false", "FORGEJO__log__LEVEL=info", "FORGEJO__mailer__ENABLED=false", ]; const container = new docker.Container( "foundation-forgejo", { name: "foundation-forgejo", image: image.imageId, hostname: "foundation-forgejo", restart: "unless-stopped", envs, volumes: [{ volumeName: volume.name, containerPath: "/data" }], networksAdvanced: [ { name: network.name, aliases: ["foundation-forgejo"] }, ], ports: [ { internal: 22, external: 22 }, // scp-form git@git.olsitec.net (the goal) { internal: 22, external: cfg.forgeSshPort }, // 2222 (CONTRACT_003) ], healthcheck: { tests: [ "CMD-SHELL", // healthz pretty-prints `"status": "pass"` (note the space); head -3 limits // the match to the top-level status, not a sub-check. "wget -qO- http://127.0.0.1:3000/api/healthz | head -3 | grep -q '\"status\": \"pass\"' || exit 1", ], interval: "15s", timeout: "5s", retries: 6, startPeriod: "40s", }, logDriver: "json-file", logOpts: { "max-size": "10m", "max-file": "3" }, }, { provider, dependsOn: [network, deps.postgres.ready, deps.rustfs.ready], deleteBeforeReplace: true, }, ); // GATE B: block downstream (T09 admin/org/repo, T10 runner) until healthz passes. const ready = new command.remote.Command( "foundation-forgejo-ready", { connection: vmConnection(ctx), create: `set -eu for _ in $(seq 1 60); do if docker exec foundation-forgejo wget -qO- http://127.0.0.1:3000/api/healthz 2>/dev/null | head -3 | grep -q '"status": "pass"'; then echo "foundation-forgejo healthy"; exit 0 fi sleep 4 done echo "foundation-forgejo did not become healthy in 240s" >&2 docker logs --tail 30 foundation-forgejo >&2 || true exit 1`, addPreviousOutputInEnv: false, triggers: [container.id], }, { dependsOn: [container] }, ); return { container, ready, endpoint: "http://foundation-forgejo:3000" }; }