feat(bootstrap): rustfs S3 data-plane + buckets/service account (T04)
foundation-rustfs (rustfs/rustfs digest-pinned) on foundation-net, internal only (9000/9001 unpublished); named volume foundation-rustfs-data with retainOnDelete. The four buckets (forgejo-packages/-artifacts/-lfs, foundation-backups) and a scoped service account with generated keys (CONTRACT_002 rustfs slice) are provisioned post-boot by an idempotent, readiness-gated remote.Command using a throwaway mc container (ADR-007). RustFS speaks enough MinIO admin API for `svcacct add`; `mc ready` is unreliable so readiness gates on `mc ls`; the mc image's busybox lacks grep so existence checks use a shell `case`. Pins the IMAGE_MC tool image in VERSIONS. Live on cx33 Helsinki: 4 buckets present, service key registered, put/get roundtrip OK, no published ports. Acceptance T04 met. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6edba60612
commit
1792fd9f89
4 changed files with 147 additions and 4 deletions
7
VERSIONS
7
VERSIONS
|
|
@ -60,10 +60,15 @@ IMAGE_CADDY=caddy:2.10@sha256:PIN_DIGEST
|
|||
IMAGE_FORGEJO=codeberg.org/forgejo/forgejo:11@sha256:PIN_DIGEST
|
||||
IMAGE_POSTGRES=postgres:17@sha256:5c855ad7b85e68e48a62f34662853f38b57c1c1d80f3a927ab58034fd6d31c5e
|
||||
IMAGE_VAULT=hashicorp/vault:1.18@sha256:PIN_DIGEST
|
||||
IMAGE_RUSTFS=rustfs/rustfs:latest@sha256:PIN_DIGEST
|
||||
IMAGE_RUSTFS=rustfs/rustfs:latest@sha256:fa19210ac4697c79d7ccca1ec9b0eb91aebacc6691991ffb14014bb3c67e6cc3
|
||||
IMAGE_ACT_RUNNER=code.forgejo.org/forgejo/runner:6@sha256:PIN_DIGEST
|
||||
IMAGE_REGISTRY=registry:2@sha256:PIN_DIGEST
|
||||
|
||||
# Tool image: MinIO client `mc` — used transiently (never a long-running service)
|
||||
# for S3 control-plane ops against RustFS: bucket creation + service accounts
|
||||
# (T04) and backup put/get (T12). RustFS speaks enough of the MinIO admin API.
|
||||
IMAGE_MC=minio/mc:latest@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727
|
||||
|
||||
# NOTE on specific images:
|
||||
# IMAGE_RUSTFS uses `latest` because RustFS does not (yet) publish stable
|
||||
# semver tags reliably (PLAN-002 R3 — RustFS is young). MUST be pinned by
|
||||
|
|
|
|||
|
|
@ -24,9 +24,18 @@ export interface PostgresCredentials {
|
|||
forgejoDbPassword: pulumi.Output<string>;
|
||||
}
|
||||
|
||||
/** `foundation/rustfs/service-credentials` (CONTRACT_002 §2.3). */
|
||||
export interface RustfsCredentials {
|
||||
adminUser: string; // "rustfsadmin" — root access key name (deterministic)
|
||||
adminPassword: pulumi.Output<string>; // root secret key (RUSTFS_SECRET_KEY)
|
||||
serviceKeyId: pulumi.Output<string>; // scoped S3 key Forgejo/backup use
|
||||
serviceKeySecret: pulumi.Output<string>;
|
||||
}
|
||||
|
||||
/** Everything generateCredentials() produces; grows as Wave-2 tasks land. */
|
||||
export interface FoundationCredentials {
|
||||
postgres: PostgresCredentials;
|
||||
rustfs: RustfsCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,5 +55,11 @@ export function generateCredentials(ctx: DeployCtx): FoundationCredentials {
|
|||
forgejoDbUser: "forgejo",
|
||||
forgejoDbPassword: secret("forgejo-db-password"),
|
||||
},
|
||||
rustfs: {
|
||||
adminUser: "rustfsadmin",
|
||||
adminPassword: secret("rustfs-admin-password"),
|
||||
serviceKeyId: secret("rustfs-service-key-id", 20), // S3 access-key id
|
||||
serviceKeySecret: secret("rustfs-service-key-secret", 40), // S3 secret
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
121
bootstrap/components/rustfs.ts
Normal file
121
bootstrap/components/rustfs.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// 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; 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" };
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { deployNetwork } from "./components/network";
|
|||
import { deployDns } from "./components/dns";
|
||||
import { generateCredentials } from "./components/credentials";
|
||||
import { deployPostgres } from "./components/postgres";
|
||||
import { deployRustfs } from "./components/rustfs";
|
||||
|
||||
const cfg = loadConfig();
|
||||
|
||||
|
|
@ -28,10 +29,10 @@ const credentials = generateCredentials(ctx);
|
|||
|
||||
// =============================================================================
|
||||
// PHASE 3 — DATA PLANE (depends on: network)
|
||||
// T03 postgres ✓ · T04 rustfs · T05 vault (sealed)
|
||||
// T03 postgres ✓ · T04 rustfs ✓ · T05 vault (sealed)
|
||||
// -----------------------------------------------------------------------------
|
||||
const postgres = deployPostgres(ctx, credentials.postgres);
|
||||
// const rustfs = deployRustfs(ctx, credentials.rustfs);
|
||||
const rustfs = deployRustfs(ctx, credentials.rustfs);
|
||||
// const vault = deployVault(ctx);
|
||||
//
|
||||
// --- GATE A: Vault init + unseal (T05) → writes unseal keys to encrypted config;
|
||||
|
|
@ -49,10 +50,11 @@ const postgres = deployPostgres(ctx, credentials.postgres);
|
|||
// =============================================================================
|
||||
|
||||
// Stack outputs (extended as phases land).
|
||||
export const phase = "T03-postgres"; // network + DNS + data-plane: postgres
|
||||
export const phase = "T04-rustfs"; // network + DNS + data-plane: postgres, rustfs
|
||||
export const networkName = network.name;
|
||||
export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`;
|
||||
export const postgresEndpoint = postgres.endpoint;
|
||||
export const rustfsEndpoint = rustfs.endpoint;
|
||||
export const enabledFeatures = Object.entries(cfg.features)
|
||||
.filter(([, on]) => on)
|
||||
.map(([name]) => name);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue