// 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 { ForgejoCredentials, 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" }; } // ─── T09: headless first-admin + org + repo + operator SSH key ─────────────── // All idempotent, over docker-exec (ADR-007): `forgejo admin` CLI for the admin // (run as the git user so /data stays git-owned), then the Forgejo API via the // image's own curl against localhost:3000. The admin password arrives on stdin // (secret); the operator SSH public key on stdin (non-secret, but has spaces). // No web installer, no default credentials (PLAN-002 §9.3). const BOOTSTRAP = `set -eu IFS= read -r ADMIN_PW IFS= read -r SSH_PUBKEY C=foundation-forgejo BASE=http://127.0.0.1:3000/api/v1 if docker exec -u git "$C" forgejo admin user list 2>/dev/null | grep -qw "$ADMIN"; then echo "admin exists" else docker exec -u git "$C" forgejo admin user create --admin --username "$ADMIN" --password "$ADMIN_PW" --email "$EMAIL" --must-change-password=false >/dev/null echo "admin created" fi au() { docker exec -i "$C" curl -fsS -u "$ADMIN:$ADMIN_PW" -H 'Content-Type: application/json' "$@"; } code() { docker exec "$C" curl -s -o /dev/null -w '%{http_code}' -u "$ADMIN:$ADMIN_PW" "$@"; } if [ "$(code "$BASE/orgs/$ORG")" = 200 ]; then echo "org exists" else au -X POST "$BASE/orgs" -d "{\\"username\\":\\"$ORG\\"}" >/dev/null echo "org created" fi if [ "$(code "$BASE/repos/$ORG/$REPO")" = 200 ]; then echo "repo exists" else au -X POST "$BASE/orgs/$ORG/repos" -d "{\\"name\\":\\"$REPO\\",\\"auto_init\\":true,\\"private\\":false,\\"default_branch\\":\\"main\\",\\"description\\":\\"olsitec-foundation platform repo\\"}" >/dev/null echo "repo created" fi if au "$BASE/user/keys" | grep -q "\\"title\\":\\"$KEYTITLE\\""; then echo "ssh key exists" else au -X POST "$BASE/user/keys" -d "{\\"title\\":\\"$KEYTITLE\\",\\"key\\":\\"$SSH_PUBKEY\\"}" >/dev/null echo "ssh key added" fi echo "forgejo bootstrap complete: $ORG/$REPO"`; export interface ForgejoBootstrapArgs { forgejo: ForgejoOutputs; adminCreds: ForgejoCredentials; acmeEmail: string; orgName: string; repoName: string; sshPublicKey: string; } /** Create the admin, the org, a seeded repo, and register the operator SSH key. */ export function bootstrapForgejo( ctx: DeployCtx, args: ForgejoBootstrapArgs, ): command.remote.Command { const create = pulumi.interpolate`ADMIN='${args.adminCreds.adminUser}' EMAIL='${args.acmeEmail}' ORG='${args.orgName}' REPO='${args.repoName}' KEYTITLE='operator-foundation' ${BOOTSTRAP}`; return new command.remote.Command( "foundation-forgejo-bootstrap", { connection: vmConnection(ctx), create, update: create, stdin: pulumi.interpolate`${args.adminCreds.adminPassword} ${args.sshPublicKey} `, addPreviousOutputInEnv: false, triggers: [ args.forgejo.ready.id, args.adminCreds.adminPassword, args.sshPublicKey, ], }, { dependsOn: [args.forgejo.ready] }, ); }