diff --git a/VERSIONS b/VERSIONS index 92fd0fb..6befa05 100644 --- a/VERSIONS +++ b/VERSIONS @@ -60,7 +60,7 @@ # caddy:2 lacks it) — recipe + pinned base digests + module version live in # containers/caddy-cloudflare/Dockerfile. This is the pinned FINAL base it derives. IMAGE_CADDY=caddy:2.10@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb -IMAGE_FORGEJO=codeberg.org/forgejo/forgejo:11@sha256:PIN_DIGEST +IMAGE_FORGEJO=codeberg.org/forgejo/forgejo:11@sha256:d98d860ea64fd36cb0aabf0b46bbe1a37566b498eee4af0a6b246d5a45759d6d IMAGE_POSTGRES=postgres:17@sha256:5c855ad7b85e68e48a62f34662853f38b57c1c1d80f3a927ab58034fd6d31c5e IMAGE_VAULT=hashicorp/vault:1.18@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166 IMAGE_RUSTFS=rustfs/rustfs:latest@sha256:fa19210ac4697c79d7ccca1ec9b0eb91aebacc6691991ffb14014bb3c67e6cc3 diff --git a/bootstrap/components/forgejo.ts b/bootstrap/components/forgejo.ts new file mode 100644 index 0000000..b0ca439 --- /dev/null +++ b/bootstrap/components/forgejo.ts @@ -0,0 +1,162 @@ +// 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" }; +} diff --git a/bootstrap/index.ts b/bootstrap/index.ts index c7a5c80..98e8733 100644 --- a/bootstrap/index.ts +++ b/bootstrap/index.ts @@ -17,6 +17,7 @@ import { deployPostgres } from "./components/postgres"; import { deployRustfs } from "./components/rustfs"; import { deployVault } from "./components/vault"; import { deployProxy } from "./components/proxy"; +import { deployForgejo } from "./components/forgejo"; const cfg = loadConfig(); @@ -48,7 +49,12 @@ const vaultCreds = writeCredentialsToVault(ctx, credentials, vault); // T07 caddy ✓ · T08 forgejo · T10 runner // ----------------------------------------------------------------------------- const proxy = deployProxy(ctx); -// const forgejo = deployForgejo(ctx, { postgres, rustfs, credentials, proxy }); +const forgejo = deployForgejo(ctx, { + postgres, + rustfs, + pgCreds: credentials.postgres, + rustfsCreds: credentials.rustfs, +}); // --- GATE B: Forgejo healthy → handover (T11) + runner registration (T10). // const runner = deployRunner(ctx, { forgejo, credentials }); // ============================================================================= @@ -57,8 +63,10 @@ const proxy = deployProxy(ctx); // vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet. void vaultCreds; -export const phase = "T07-caddy"; // data-plane + creds + public ingress (Caddy) +export const phase = "T08-forgejo"; // forge live (postgres + rustfs + caddy + ssh) export const caddyImageId = proxy.imageId; +export const forgejoEndpoint = forgejo.endpoint; +void forgejo.ready; // GATE B for T09/T10 export const networkName = network.name; export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`; export const postgresEndpoint = postgres.endpoint;