// components/rustfs.ts (T04) // // foundation-rustfs — the Layer-0 S3 (CONTRACT_003 §3.2; ADR-004 RustFS primary). // Holds Forgejo blobs (LFS, packages, Actions artifacts) + backup bundles. // Internal-only (9000 S3 / 9001 console NOT published; Caddy may expose S3 at // s3.olsitec.net later). Named volume foundation-rustfs-data → /data // (CONTRACT_003 §3.4; bucket-level backup, CONTRACT_004). // // RustFS root creds = RUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEY (generateCredentials). // Buckets + a scoped service key (for Forgejo/backup) are provisioned post-boot // by an idempotent, readiness-gated remote.Command using a throwaway `mc` // container on foundation-net (ADR-007) — RustFS speaks the MinIO S3 + admin API // (verified: mb/cp/svcacct work; `mc ready` does not, so readiness gates on `mc ls`). 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 { RustfsCredentials } from "./credentials"; export interface RustfsOutputs { container: docker.Container; /** Buckets + service account exist once this resolves — gates Forgejo (T08). */ ready: command.remote.Command; /** Internal S3 endpoint for app.ini / consumers (CONTRACT_003 §3.3). */ endpoint: string; } // Idempotent, readiness-gated bucket + service-account setup (ADR-007). The four // values (root access key + secret, service key id + secret) arrive on stdin; the // non-secret MinIO-client image ref ($MC_IMAGE) is prepended by the caller. mc // reads MC_HOST_rfs for auth; creds reach the throwaway mc container via -e only. const BUCKET_SETUP = `set -eu IFS= read -r ROOT_AK IFS= read -r ROOT_SK IFS= read -r SVC_AK IFS= read -r SVC_SK MCHOST="http://$ROOT_AK:$ROOT_SK@foundation-rustfs:9000" ready= for _ in $(seq 1 40); do if docker run --rm --network foundation-net --entrypoint sh -e MC_HOST_rfs="$MCHOST" "$MC_IMAGE" -c 'mc ls rfs/ >/dev/null 2>&1'; then ready=1; break fi sleep 2 done [ "$ready" = 1 ] || { echo "foundation-rustfs not ready after 80s" >&2; exit 1; } docker run --rm --network foundation-net --entrypoint sh \ -e MC_HOST_rfs="$MCHOST" -e ROOT_AK="$ROOT_AK" -e SVC_AK="$SVC_AK" -e SVC_SK="$SVC_SK" \ "$MC_IMAGE" -c ' set -e for b in forgejo-packages forgejo-artifacts forgejo-lfs foundation-backups foundation-ci-state; do mc mb --ignore-existing "rfs/$b" done EXISTING=$(mc admin user svcacct ls rfs "$ROOT_AK" 2>/dev/null || true) case "$EXISTING" in *"$SVC_AK"*) echo "svcacct exists" ;; *) mc admin user svcacct add --access-key "$SVC_AK" --secret-key "$SVC_SK" rfs "$ROOT_AK" >/dev/null; echo "svcacct created" ;; esac echo "buckets:"; mc ls rfs/ ' echo "foundation-rustfs: buckets + service account ready"`; export function deployRustfs( ctx: DeployCtx, creds: RustfsCredentials, ): RustfsOutputs { const { provider, network } = ctx; const image = new docker.RemoteImage( "foundation-rustfs-image", { name: ctx.image("RUSTFS"), keepLocally: true }, { provider }, ); const volume = new docker.Volume( "foundation-rustfs-data", { name: "foundation-rustfs-data" }, { provider, retainOnDelete: true }, // blobs — never silently drop ); const container = new docker.Container( "foundation-rustfs", { name: "foundation-rustfs", image: image.imageId, hostname: "foundation-rustfs", restart: "unless-stopped", envs: [ pulumi.interpolate`RUSTFS_ACCESS_KEY=${creds.adminUser}`, pulumi.interpolate`RUSTFS_SECRET_KEY=${creds.adminPassword}`, "RUSTFS_VOLUMES=/data", ], volumes: [{ volumeName: volume.name, containerPath: "/data" }], networksAdvanced: [{ name: network.name, aliases: ["foundation-rustfs"] }], logDriver: "json-file", logOpts: { "max-size": "10m", "max-file": "3" }, }, { provider, dependsOn: [network], deleteBeforeReplace: true }, ); const create = pulumi.interpolate`MC_IMAGE='${ctx.image("MC")}' ${BUCKET_SETUP}`; const ready = new command.remote.Command( "foundation-rustfs-buckets", { connection: vmConnection(ctx), create, // stdin order MUST match the `read`s in BUCKET_SETUP; trailing newline so // the final read sees EOL not EOF under `set -e`. stdin: pulumi.interpolate`${creds.adminUser}\n${creds.adminPassword}\n${creds.serviceKeyId}\n${creds.serviceKeySecret}\n`, addPreviousOutputInEnv: false, triggers: [container.id, creds.serviceKeyId, creds.serviceKeySecret], }, { dependsOn: [container] }, ); return { container, ready, endpoint: "http://foundation-rustfs:9000" }; }