feat(bootstrap): shared docker provider + foundation-net precursor (ADR-006)
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) <noreply@anthropic.com>
This commit is contained in:
parent
57c4eadea7
commit
6a29db386f
8 changed files with 287 additions and 32 deletions
58
bootstrap/lib/context.ts
Normal file
58
bootstrap/lib/context.ts
Normal file
|
|
@ -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<T> = (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 };
|
||||
}
|
||||
59
bootstrap/lib/versions.ts
Normal file
59
bootstrap/lib/versions.ts
Normal file
|
|
@ -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:<digest>`.
|
||||
//
|
||||
// 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<string, string> | undefined;
|
||||
|
||||
function load(): Record<string, string> {
|
||||
if (cache) return cache;
|
||||
const text = fs.readFileSync(VERSIONS_PATH, "utf8");
|
||||
const out: Record<string, string> = {};
|
||||
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`];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue