From 6a29db386f09a159025edb1a9ee776f26bb2de35 Mon Sep 17 00:00:00 2001 From: Andreas Niemann Date: Tue, 30 Jun 2026 18:18:40 +0200 Subject: [PATCH] feat(bootstrap): shared docker provider + foundation-net precursor (ADR-006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composition substrate for Wave 2 (T03+): - lib/context.ts: one Docker-over-SSH provider + DeployCtx threaded to component factories; FOUNDATION_DOCKER_HOST override for ephemeral validation. - lib/versions.ts: resolve pinned images from VERSIONS; FOUNDATION_ALLOW_UNPINNED for local validation when digests are still PIN_DIGEST. - components/network.ts: foundation-net (CONTRACT_003 §3.1). - index.ts: phase-orchestration entrypoint with dependsOn gates; Wave-2 slots. - ADR-006: shared-provider + per-component-factory model (egg does not route its phased bootstrap through the monolithic vendored DockerDeployments). Validated: pulumi up over Docker-over-SSH created+verified+destroyed foundation-net on crunchy01 (x86_64); ephemeral, nothing persisted. tsc + preview clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- bootstrap/components/network.ts | 21 ++++++ bootstrap/index.ts | 73 +++++++++++-------- bootstrap/lib/context.ts | 58 +++++++++++++++ bootstrap/lib/versions.ts | 59 +++++++++++++++ bootstrap/package.json | 1 + bun.lock | 39 +++++++++- .../2026-06-30_foundation-bootstrap.log | 12 +++ .../ADR_006_bootstrap_composition_model.md | 56 ++++++++++++++ 8 files changed, 287 insertions(+), 32 deletions(-) create mode 100644 bootstrap/components/network.ts create mode 100644 bootstrap/lib/context.ts create mode 100644 bootstrap/lib/versions.ts create mode 100644 documentation/command-log/2026-06-30_foundation-bootstrap.log create mode 100644 documentation/decisions/ADR_006_bootstrap_composition_model.md diff --git a/bootstrap/components/network.ts b/bootstrap/components/network.ts new file mode 100644 index 0000000..e9a7990 --- /dev/null +++ b/bootstrap/components/network.ts @@ -0,0 +1,21 @@ +// components/network.ts +// +// The foundation-net user-defined bridge (CONTRACT_003 §3.1). Created once, on the +// shared provider; every service container attaches to it and reaches peers by +// container name via Docker's embedded DNS. This is the first thing the bootstrap +// creates — all data-plane and forge components depend on it. +import * as docker from "@pulumi/docker"; +import { BaseCtx } from "../lib/context"; + +export function deployNetwork(ctx: BaseCtx): docker.Network { + return new docker.Network( + "foundation-net", + { + name: ctx.cfg.network.name, // "foundation-net" (CONTRACT_003) + driver: "bridge", + attachable: true, + ipamConfigs: [{ subnet: ctx.cfg.network.subnet }], // "172.30.0.0/24" + }, + { provider: ctx.provider, deleteBeforeReplace: true }, + ); +} diff --git a/bootstrap/index.ts b/bootstrap/index.ts index d3bb29c..64d37b7 100644 --- a/bootstrap/index.ts +++ b/bootstrap/index.ts @@ -1,37 +1,50 @@ -// index.ts — the foundation egg entrypoint (PLAN-002 §0, Layer 0). +// index.ts — foundation bootstrap entrypoint (the egg, PLAN-002 §0, Layer 0). // -// T02 SCAFFOLD STATE: this entrypoint is intentionally a NO-OP beyond config -// validation. It calls loadConfig() so that `pulumi preview` exercises the -// CONTRACT_001 fail-closed validation (acceptance T02), but it creates NO real -// resources yet. The data plane / Vault / RustFS / Postgres / Forgejo / Caddy / -// runner components are LATER tasks (PLAN-002 §10: T03–T15) and are deliberately -// NOT authored here. -import * as pulumi from "@pulumi/pulumi"; +// Phase orchestration (PLAN-002 §2, §5). Components are created against the shared +// provider + foundation-net (ADR-006); GATES between phases are Pulumi dependsOn +// edges, NOT imperative sequencing, so `pulumi up` derives the order. Wave-2+ tasks +// fill the marked slots — this file is the single composition point; components stay +// pure factories in components/*. +import { loadConfig } from "./config"; +import { buildBaseContext, DeployCtx } from "./lib/context"; +import { deployNetwork } from "./components/network"; -import { loadConfig, sshPrivateKeyPath } from "./config"; +const cfg = loadConfig(); -// Fail closed here: if required config is missing/malformed, loadConfig throws -// and `pulumi preview` reports the full gap (CONTRACT_001 §Validation). -const config = loadConfig(); +// --- shared substrate: provider + network (always first) --- +const base = buildBaseContext(cfg); +const network = deployNetwork(base); +const ctx: DeployCtx = { ...base, network }; -// The vendored @olsitec/pulumi-docker provider (CONTRACT_003) will, in T03+, use -// this key path + config.vm.{host,user} to reach the foundation VM over SSH. -// Resolved here only to prove the ENV channel is wired; not yet consumed. -const sshKeyPath = sshPrivateKeyPath(); +// ============================================================================= +// PHASE 3 — DATA PLANE (depends on: network) +// T03 postgres · T04 rustfs · T05 vault (sealed) +// ----------------------------------------------------------------------------- +// const postgres = deployPostgres(ctx); +// const rustfs = deployRustfs(ctx); +// const vault = deployVault(ctx); +// +// --- GATE A: Vault init + unseal (T05) → writes unseal keys to encrypted config; +// credentials.ts (T06) dependsOn the init resource. +// const credentials = deployCredentials(ctx, { postgres, rustfs, vault }); +// +// ============================================================================= +// PHASE 6 — FORGE (depends on: credentials, GATE A) +// T07 caddy · T08 forgejo · T10 runner +// ----------------------------------------------------------------------------- +// const proxy = deployProxy(ctx); +// const forgejo = deployForgejo(ctx, { postgres, rustfs, credentials, proxy }); +// --- GATE B: Forgejo healthy → handover (T11) + runner registration (T10). +// const runner = deployRunner(ctx, { forgejo, credentials }); +// ============================================================================= -pulumi.log.info( - `foundation config loaded (no-op scaffold): ` + - `baseDomain=${config.baseDomain}, vm=${config.vm.user}@${config.vm.host}, ` + - `network=${config.network.name} (${config.network.subnet}), tls=${config.tls.mode}`, -); - -// Stack outputs — safe, non-secret echoes so `pulumi preview`/`up` has something -// to show while no resources exist. Replaced by real component outputs in T03+. -export const phase = "T02-scaffold"; -export const baseDomain = config.baseDomain; -export const networkName = config.network.name; -export const vmTarget = pulumi.interpolate`${config.vm.user}@${config.vm.host}`; -export const sshKeyConfigured = sshKeyPath.length > 0; -export const enabledFeatures = Object.entries(config.features) +// Stack outputs (extended as phases land). +export const phase = "T03-precursor"; // network + shared provider only +export const networkName = network.name; +export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`; +export const enabledFeatures = Object.entries(cfg.features) .filter(([, on]) => on) .map(([name]) => name); + +// ctx is consumed by the Wave-2 slots above once uncommented. +void ctx; diff --git a/bootstrap/lib/context.ts b/bootstrap/lib/context.ts new file mode 100644 index 0000000..a603151 --- /dev/null +++ b/bootstrap/lib/context.ts @@ -0,0 +1,58 @@ +// lib/context.ts +// +// The shared deploy context every component receives (ADR-006). The bootstrap +// creates ONE docker.Provider (Docker-over-SSH to the foundation VM) and ONE +// foundation-net network; components then create their own container(s) against +// this shared provider/network, which gives the bootstrap full control over +// ordering and the phase GATES (e.g. Vault init between data-plane and Forgejo) +// that the vendored monolithic DockerDeployments cannot express. +// +// Validation override: the committed Pulumi.foundation.yaml carries placeholder +// VM coordinates (RFC-5737). For local/ephemeral validation against a dev Docker +// host, export FOUNDATION_DOCKER_HOST=ssh://user@host to point the provider there +// without editing committed config. +import * as pulumi from "@pulumi/pulumi"; +import * as docker from "@pulumi/docker"; +import { FoundationConfig, sshPrivateKeyPath } from "../config"; +import { image } from "./versions"; + +/** Base context: shared provider + helpers, before the network exists. */ +export interface BaseCtx { + cfg: FoundationConfig; + provider: docker.Provider; + sshKeyPath: string; + /** Resolve a pinned image by VERSIONS key suffix, e.g. ctx.image("POSTGRES"). */ + image: (name: string) => string; +} + +/** Full context handed to every component: base + the shared network. */ +export interface DeployCtx extends BaseCtx { + network: docker.Network; +} + +/** Signature every Wave-2+ component factory follows (ADR-006). */ +export type ComponentFactory = (ctx: DeployCtx) => T; + +function providerHost(cfg: FoundationConfig): string { + return process.env.FOUNDATION_DOCKER_HOST || `ssh://${cfg.vm.user}@${cfg.vm.host}`; +} + +/** + * Build the shared Docker-over-SSH provider. SSH options mirror the vendored + * pulumi-docker wrapper (non-interactive host-key handling for automation). + */ +export function buildBaseContext(cfg: FoundationConfig): BaseCtx { + const sshKeyPath = sshPrivateKeyPath(); + const provider = new docker.Provider("foundation-host", { + host: providerHost(cfg), + sshOpts: [ + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-i", + sshKeyPath, + ], + }); + return { cfg, provider, sshKeyPath, image }; +} diff --git a/bootstrap/lib/versions.ts b/bootstrap/lib/versions.ts new file mode 100644 index 0000000..0dbb38f --- /dev/null +++ b/bootstrap/lib/versions.ts @@ -0,0 +1,59 @@ +// lib/versions.ts +// +// Reads foundation/VERSIONS (the determinism pin-file, T01 / baseline D5) and +// resolves an image reference by key. Image refs live in VERSIONS, NOT in Pulumi +// config (config.ts comment; CONTRACT_001 §1.1). Components ask for an image by +// its logical name; this module returns the pinned `repo:tag@sha256:`. +// +// PIN_DIGEST handling (D5 "no floating tags"): +// VERSIONS may carry `@sha256:PIN_DIGEST` placeholders until digests are pinned +// online (T01 documented the `pin-digests` procedure). For a real deploy that is +// a hard error. For local/ephemeral VALIDATION against a dev Docker host, export +// FOUNDATION_ALLOW_UNPINNED=1 to fall back to the bare tag (the digest is dropped). +import * as fs from "fs"; +import * as path from "path"; + +const VERSIONS_PATH = path.resolve(__dirname, "..", "..", "VERSIONS"); + +let cache: Record | undefined; + +function load(): Record { + if (cache) return cache; + const text = fs.readFileSync(VERSIONS_PATH, "utf8"); + const out: Record = {}; + for (const raw of text.split("\n")) { + const line = raw.trim(); + if (line === "" || line.startsWith("#")) continue; + const eq = line.indexOf("="); + if (eq < 0) continue; + out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim(); + } + cache = out; + return out; +} + +/** Resolve a container image by its VERSIONS key suffix, e.g. image("FORGEJO"). */ +export function image(name: string): string { + const key = `IMAGE_${name.toUpperCase()}`; + const v = load()[key]; + if (!v) { + throw new Error(`VERSIONS: ${key} is not defined (foundation/VERSIONS)`); + } + if (v.includes("PIN_DIGEST")) { + if (process.env.FOUNDATION_ALLOW_UNPINNED === "1") { + // validation only: drop the unresolved @sha256:PIN_DIGEST, use the tag + return v.replace(/@sha256:PIN_DIGEST$/, ""); + } + throw new Error( + `VERSIONS: ${key} is not digest-pinned ('${v}'). Run the pin-digests ` + + `procedure (see VERSIONS header) before a real deploy, or set ` + + `FOUNDATION_ALLOW_UNPINNED=1 for local validation.`, + ); + } + return v; +} + +/** Minimum version string for a host tool, e.g. toolMin("VAULT"). */ +export function toolMin(name: string): string | undefined { + return load()[`TOOL_${name.toUpperCase()}_MIN`]; +} diff --git a/bootstrap/package.json b/bootstrap/package.json index f92b7e4..da5f8f9 100644 --- a/bootstrap/package.json +++ b/bootstrap/package.json @@ -7,6 +7,7 @@ "dependencies": { "@olsitec/pulumi-docker": "workspace:*", "@olsitec/pulumi-vault": "workspace:*", + "@pulumi/docker": "^4.5.8", "@pulumi/pulumi": "^3.138.0" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index 1012987..a7fdc9a 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "dependencies": { "@olsitec/pulumi-docker": "workspace:*", "@olsitec/pulumi-vault": "workspace:*", + "@pulumi/docker": "^4.5.8", "@pulumi/pulumi": "^3.138.0", }, "devDependencies": { @@ -584,7 +585,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -690,18 +691,32 @@ "@npmcli/arborist/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@npmcli/arborist/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@npmcli/fs/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "@npmcli/git/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@npmcli/git/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], "@npmcli/map-workspaces/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@npmcli/metavuln-calculator/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@npmcli/package-json/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@npmcli/promise-spawn/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], - "@pulumi/docker/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "@opentelemetry/instrumentation/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "@opentelemetry/sdk-trace-node/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "@pulumi/eslint-plugin/typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + "@pulumi/pulumi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@tufjs/models/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg=="], @@ -720,8 +735,12 @@ "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + "@typescript-eslint/utils/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -750,10 +769,20 @@ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "normalize-package-data/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "npm-install-checks/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "npm-package-arg/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "npm-pick-manifest/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], @@ -782,6 +811,8 @@ "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], @@ -812,8 +843,12 @@ "@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/documentation/command-log/2026-06-30_foundation-bootstrap.log b/documentation/command-log/2026-06-30_foundation-bootstrap.log new file mode 100644 index 0000000..5c5574a --- /dev/null +++ b/documentation/command-log/2026-06-30_foundation-bootstrap.log @@ -0,0 +1,12 @@ +--- 2026-06-30T16:17:52Z --- +HOST: mac-studio -> docker:ssh://andiolsi@192.168.1.2 (crunchy01) +CWD: /Users/andiolsi/work/olsitec-foundation/foundation/bootstrap +REPO: olsitec-foundation BRANCH: master ENVIRONMENT: validation(ephemeral) +CMD: pulumi up --yes (create foundation-net only) then inspect then pulumi destroy --yes +EXIT: RUNNING +NOTE: T03 precursor live validation — prove Docker-over-SSH provider creates foundation-net on crunchy01; ephemeral, destroyed immediately, nothing persisted (per user constraint). +--- +--- 2026-06-30T16:18:39Z UPDATE --- +CMD: (above) pulumi up/inspect/destroy — foundation-net on crunchy01 +EXIT: 0 — created (subnet 172.30.0.0/24, bridge, attachable, verified) then destroyed clean; nothing persisted. +--- diff --git a/documentation/decisions/ADR_006_bootstrap_composition_model.md b/documentation/decisions/ADR_006_bootstrap_composition_model.md new file mode 100644 index 0000000..15af35e --- /dev/null +++ b/documentation/decisions/ADR_006_bootstrap_composition_model.md @@ -0,0 +1,56 @@ +# ADR-006 — Bootstrap Composition Model: Shared Provider + Per-Component Factories + +**Date**: 2026-06-30 +**Status**: Accepted + +## Context + +The vendored `@olsitec/pulumi-docker` `DockerDeployments` class (T02) is **monolithic**: its +constructor takes the full provider map + a flat list of all containers and creates them together, +with no mechanism for ordering between containers beyond the network. The foundation bootstrap, +however, needs **phase gates** (PLAN-002 §2/§5): Vault must be `operator init`'d and unsealed +*between* the data-plane and Forgejo consuming its secrets, and Forgejo must be healthy *before* +runner registration/handover. Expressing those gates through the monolithic wrapper is awkward +(it would create the provider/network multiple times and cannot interleave an imperative init step). + +## Decision + +The bootstrap creates the **shared `docker.Provider` and `foundation-net` once** (`bootstrap/lib/context.ts`, +`bootstrap/components/network.ts`), and each service is a **pure factory** in +`bootstrap/components/.ts` with signature `(ctx: DeployCtx) => ` that creates its own +`docker.Container`(s) against the shared provider/network. `bootstrap/index.ts` is the single +composition point; **phase gates are Pulumi `dependsOn` edges**, not imperative sequencing, so +`pulumi up` derives the order. + +The vendored `DockerDeployments` is **retained** as the published `@olsitec/pulumi-docker` API for +simple *downstream* (Layer-1) use — deploying a flat list of containers — but the egg does **not** +route its phased bootstrap through it. + +Validation override: `FOUNDATION_DOCKER_HOST=ssh://user@host` points the provider at a dev Docker +host without editing committed config; `FOUNDATION_ALLOW_UNPINNED=1` lets `VERSIONS` PIN_DIGEST +placeholders fall back to tags for local validation only. + +## Consequences + +**Easier**: +- Phase gates (Vault init, Forgejo-before-handover) are natural dependency edges. +- One file per service, each independently ownable by a Wave-2+ agent (clean parallelism). +- Validated end-to-end: `pulumi up` over Docker-over-SSH created `foundation-net` on crunchy01 + (172.30.0.0/24, attachable) and `destroy` removed it cleanly (2026-06-30). + +**Harder**: +- More explicit plumbing than the one-call wrapper (a shared `ctx` threaded through factories). +- Two composition styles coexist (egg = direct; downstream = `DockerDeployments`) — documented here + to avoid confusion. + +## Alternatives Considered + +- **Route the whole bootstrap through `DockerDeployments`**: rejected — cannot express phase gates + or the imperative Vault-init step without recreating the provider/network per call. +- **Modify the vendored module to support phases**: rejected — it must stay a faithful Stage-1 vendor + (ADR-005) and remain useful as the simple downstream API; forking its behaviour now splits it. + +## Confidence + +**High** — the model is implemented and the Docker-over-SSH path is proven against a real x86_64 +host (crunchy01). Companion: CONTRACT_003, PLAN-002 §5.