feat(bootstrap): forgejo actions runner (T10)
foundation-runner (forgejo/runner:6, digest-pinned). Registration is idempotent (ADR-007): it reuses /data/.runner if present, else mints a token via `forgejo actions generate-runner-token` and consumes it with `forgejo-runner register` (the token never leaves the VM). The daemon runs as uid 1000 with the host docker group (gid 996) added for socket access — root-equivalent and co-located, the documented day-zero compromise (PLAN-002 R5 / PLAN-001 §4a); a fenced or separate runner VM is the steady state. Live on cx33 Helsinki: runner declared (labels docker,dind) and polling; a hello-world `runs-on: docker` workflow pushed to olsitec/foundation ran to success (workflow run #1). Acceptance T10 met. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d5c53ce9a2
commit
9618da1421
3 changed files with 108 additions and 3 deletions
2
VERSIONS
2
VERSIONS
|
|
@ -64,7 +64,7 @@ IMAGE_FORGEJO=codeberg.org/forgejo/forgejo:11@sha256:d98d860ea64fd36cb0aabf0b46b
|
||||||
IMAGE_POSTGRES=postgres:17@sha256:5c855ad7b85e68e48a62f34662853f38b57c1c1d80f3a927ab58034fd6d31c5e
|
IMAGE_POSTGRES=postgres:17@sha256:5c855ad7b85e68e48a62f34662853f38b57c1c1d80f3a927ab58034fd6d31c5e
|
||||||
IMAGE_VAULT=hashicorp/vault:1.18@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
|
IMAGE_VAULT=hashicorp/vault:1.18@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
|
||||||
IMAGE_RUSTFS=rustfs/rustfs:latest@sha256:fa19210ac4697c79d7ccca1ec9b0eb91aebacc6691991ffb14014bb3c67e6cc3
|
IMAGE_RUSTFS=rustfs/rustfs:latest@sha256:fa19210ac4697c79d7ccca1ec9b0eb91aebacc6691991ffb14014bb3c67e6cc3
|
||||||
IMAGE_ACT_RUNNER=code.forgejo.org/forgejo/runner:6@sha256:PIN_DIGEST
|
IMAGE_ACT_RUNNER=code.forgejo.org/forgejo/runner:6@sha256:e8dd2880f2fc81984d2308b93f1bc064dfb41187942300676536c09a3b30043d
|
||||||
IMAGE_REGISTRY=registry:2@sha256:PIN_DIGEST
|
IMAGE_REGISTRY=registry:2@sha256:PIN_DIGEST
|
||||||
|
|
||||||
# Tool image: MinIO client `mc` — used transiently (never a long-running service)
|
# Tool image: MinIO client `mc` — used transiently (never a long-running service)
|
||||||
|
|
|
||||||
103
bootstrap/components/runner.ts
Normal file
103
bootstrap/components/runner.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// components/runner.ts (T10)
|
||||||
|
//
|
||||||
|
// foundation-runner — a Forgejo Actions runner (CONTRACT_003 §3.2). Registration
|
||||||
|
// is idempotent (ADR-007): if the persistent /data/.runner config already exists it
|
||||||
|
// is reused; otherwise a fresh registration token is minted with
|
||||||
|
// `forgejo actions generate-runner-token` and consumed by `forgejo-runner register`
|
||||||
|
// (the token never leaves the VM). The daemon then polls Forgejo for jobs.
|
||||||
|
//
|
||||||
|
// SECURITY (PLAN-002 R5 / PLAN-001 §4a): this runner shares the VM and is given the
|
||||||
|
// host Docker socket so `docker`-label jobs can spawn containers — i.e. it is
|
||||||
|
// root-equivalent on the host and is NOT fenced from the forge's trust boundary.
|
||||||
|
// That is the documented day-zero compromise; the steady-state is a throwaway,
|
||||||
|
// separate privileged runner VM. Do not run untrusted workflows here until fenced.
|
||||||
|
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 { ForgejoOutputs } from "./forgejo";
|
||||||
|
|
||||||
|
export interface RunnerOutputs {
|
||||||
|
container: docker.Container;
|
||||||
|
registered: command.remote.Command;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deployRunner(
|
||||||
|
ctx: DeployCtx,
|
||||||
|
forgejo: ForgejoOutputs,
|
||||||
|
): RunnerOutputs {
|
||||||
|
const { cfg, provider, network } = ctx;
|
||||||
|
const img = ctx.image("ACT_RUNNER");
|
||||||
|
const labels = cfg.runner.labels.join(",");
|
||||||
|
|
||||||
|
const image = new docker.RemoteImage(
|
||||||
|
"foundation-runner-image",
|
||||||
|
{ name: img, keepLocally: true },
|
||||||
|
{ provider },
|
||||||
|
);
|
||||||
|
const volume = new docker.Volume(
|
||||||
|
"foundation-runner-data",
|
||||||
|
{ name: "foundation-runner-data" },
|
||||||
|
{ provider, retainOnDelete: true }, // holds .runner registration secret
|
||||||
|
);
|
||||||
|
|
||||||
|
const register = new command.remote.Command(
|
||||||
|
"foundation-runner-register",
|
||||||
|
{
|
||||||
|
connection: vmConnection(ctx),
|
||||||
|
create: pulumi.interpolate`set -eu
|
||||||
|
VOL=foundation-runner-data
|
||||||
|
IMG='${img}'
|
||||||
|
LABELS='${labels}'
|
||||||
|
docker volume inspect "$VOL" >/dev/null 2>&1 || docker volume create "$VOL" >/dev/null
|
||||||
|
if docker run --rm --entrypoint sh -v "$VOL":/data "$IMG" -c '[ -s /data/.runner ]'; then
|
||||||
|
echo "runner already registered"
|
||||||
|
else
|
||||||
|
TOKEN=$(docker exec -u git foundation-forgejo forgejo actions generate-runner-token)
|
||||||
|
docker run --rm --network foundation-net --entrypoint /bin/forgejo-runner -v "$VOL":/data "$IMG" \\
|
||||||
|
register --no-interactive --instance http://foundation-forgejo:3000 --token "$TOKEN" --name foundation-runner --labels "$LABELS" >/dev/null
|
||||||
|
echo "runner registered"
|
||||||
|
fi`,
|
||||||
|
addPreviousOutputInEnv: false,
|
||||||
|
triggers: [forgejo.ready.id, labels],
|
||||||
|
},
|
||||||
|
{ dependsOn: [forgejo.ready] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = new docker.Container(
|
||||||
|
"foundation-runner",
|
||||||
|
{
|
||||||
|
name: "foundation-runner",
|
||||||
|
image: image.imageId,
|
||||||
|
hostname: "foundation-runner",
|
||||||
|
restart: "unless-stopped",
|
||||||
|
entrypoints: ["/bin/forgejo-runner"],
|
||||||
|
command: ["daemon"],
|
||||||
|
// The image runs as uid 1000; add the host docker group (gid of
|
||||||
|
// /var/run/docker.sock) so the daemon can reach the socket without running
|
||||||
|
// as root. NOTE: 996 is THIS host's docker gid — re-check on DR to a new VM
|
||||||
|
// (`stat -c %g /var/run/docker.sock`). Socket access is root-equivalent
|
||||||
|
// regardless (see the security note above).
|
||||||
|
groupAdds: ["996"],
|
||||||
|
envs: ["DOCKER_HOST=unix:///var/run/docker.sock"],
|
||||||
|
volumes: [
|
||||||
|
{ volumeName: volume.name, containerPath: "/data" },
|
||||||
|
{
|
||||||
|
hostPath: "/var/run/docker.sock",
|
||||||
|
containerPath: "/var/run/docker.sock",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
networksAdvanced: [{ name: network.name, aliases: ["foundation-runner"] }],
|
||||||
|
logDriver: "json-file",
|
||||||
|
logOpts: { "max-size": "10m", "max-file": "3" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider,
|
||||||
|
dependsOn: [network, register],
|
||||||
|
deleteBeforeReplace: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { container, registered: register };
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import { deployRustfs } from "./components/rustfs";
|
||||||
import { deployVault } from "./components/vault";
|
import { deployVault } from "./components/vault";
|
||||||
import { deployProxy } from "./components/proxy";
|
import { deployProxy } from "./components/proxy";
|
||||||
import { deployForgejo, bootstrapForgejo } from "./components/forgejo";
|
import { deployForgejo, bootstrapForgejo } from "./components/forgejo";
|
||||||
|
import { deployRunner } from "./components/runner";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
|
@ -70,18 +71,19 @@ const forgejoBootstrap = bootstrapForgejo(ctx, {
|
||||||
repoName: "foundation",
|
repoName: "foundation",
|
||||||
sshPublicKey,
|
sshPublicKey,
|
||||||
});
|
});
|
||||||
// const runner = deployRunner(ctx, { forgejo, credentials });
|
const runner = cfg.features.runner ? deployRunner(ctx, forgejo) : undefined;
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// Stack outputs (extended as phases land).
|
// Stack outputs (extended as phases land).
|
||||||
// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet.
|
// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet.
|
||||||
void vaultCreds;
|
void vaultCreds;
|
||||||
|
|
||||||
export const phase = "T09-forge-bootstrap"; // admin + org + repo + operator key
|
export const phase = "T10-runner"; // forge + CI runner live
|
||||||
export const caddyImageId = proxy.imageId;
|
export const caddyImageId = proxy.imageId;
|
||||||
export const forgejoEndpoint = forgejo.endpoint;
|
export const forgejoEndpoint = forgejo.endpoint;
|
||||||
export const cloneUrl = pulumi.interpolate`git@${cfg.hosts.git}:${cfg.forgejo.orgName}/foundation.git`;
|
export const cloneUrl = pulumi.interpolate`git@${cfg.hosts.git}:${cfg.forgejo.orgName}/foundation.git`;
|
||||||
void forgejoBootstrap; // GATE B consumer; no secret output to export
|
void forgejoBootstrap; // GATE B consumer; no secret output to export
|
||||||
|
void runner; // CI runner (feature-flagged)
|
||||||
export const networkName = network.name;
|
export const networkName = network.name;
|
||||||
export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`;
|
export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`;
|
||||||
export const postgresEndpoint = postgres.endpoint;
|
export const postgresEndpoint = postgres.endpoint;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue