From 779f890b2f285fc142454a45f56917241175ea29 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:34:24 +0000 Subject: [PATCH] Refactored the 1,000+ line setup.ts into sub-modules - Extracted CLI commands into infra/setup/cli.ts - Extracted docker compose configuration to infra/setup/compose.ts - Extracted env variable generation to infra/setup/env.ts - Extracted build commands to infra/setup/build.ts - Retained infra/setup.ts as a simple entrypoint orchestrator Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com> --- infra/setup.ts | 1019 +---------------- infra/setup/build.ts | 73 ++ infra/setup/cli.ts | 771 +++++++++++++ infra/setup/compose.ts | 104 ++ infra/setup/env.ts | 95 ++ ...monolith-decomposition-roadmap-1845.ph4.md | 0 6 files changed, 1045 insertions(+), 1017 deletions(-) create mode 100644 infra/setup/build.ts create mode 100644 infra/setup/cli.ts create mode 100644 infra/setup/compose.ts create mode 100644 infra/setup/env.ts rename tasks/{new => complete}/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.md (100%) diff --git a/infra/setup.ts b/infra/setup.ts index 01e31ab..cfbdfe5 100644 --- a/infra/setup.ts +++ b/infra/setup.ts @@ -1,1020 +1,5 @@ -import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; -import { Input, Secret, Select } from "jsr:@cliffy/prompt@1.0.0-rc.7"; -import * as colors from "jsr:@std/fmt@0.225.2/colors"; -import * as path from "jsr:@std/path@0.225.2"; - -const ENV_PATH = path.join("infra", "stack.env"); -const SPIRE_ENV_PATH = path.join("infra", ".env.spire"); -const COMPOSE_PATH = path.join("infra", "compose.yml"); -const SPIRE_COMPOSE_PATH = path.join("infra", "compose.spire.yml"); - -export interface AuthSetupConfig { - reg: string; - ghcrReg: string; - domainName: string; - dbPassword: string; - dbDataPath: string; - spireDataPath: string; - appSecret: string; -} - -const DEFAULT_AUTH_CONFIG: AuthSetupConfig = { - reg: "quay.atyg.org", - ghcrReg: "ghcr.atyg.org", - domainName: "auth.system.local", - dbPassword: "", - dbDataPath: "/volume1/docker/auth-yes/data", - spireDataPath: "/volume1/docker/spire", - appSecret: "", -}; - -export async function readEnv(): Promise> { - const config: Partial = {}; - for ( - const filePath of [ - ENV_PATH, - path.join("infra", ".env"), - SPIRE_ENV_PATH, - path.join("infra", "stack.env.spire"), - ] - ) { - try { - const text = await Deno.readTextFile(filePath); - for (const line of text.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const [key, ...rest] = trimmed.split("="); - const val = rest.join("=").trim(); - if (key === "REG") config.reg = val; - if (key === "GHCR_REG") config.ghcrReg = val; - if (key === "SYSTEM_DOMAIN") config.domainName = val; - if (key === "POSTGRES_PASSWORD") config.dbPassword = val; - if (key === "DB_DATA_PATH") config.dbDataPath = val; - if (key === "SPIRE_DATA_PATH") config.spireDataPath = val; - if (key === "APP_SECRET") config.appSecret = val; - } - } catch (_e) { - // Ignore and check next - } - } - return config; -} - -export function generateEnv(config: AuthSetupConfig): string { - return `# --- Container Registry --- -REG=${config.reg} - -# --- Network & Routing --- -SYSTEM_DOMAIN=${config.domainName} -RP_ID=${config.domainName} -ORIGIN=https://${config.domainName} - -# --- Identity Provider Internal App Secret --- -APP_SECRET=${config.appSecret} - -# --- Database Configuration --- -POSTGRES_HOST=auth-db -POSTGRES_PORT=5432 -POSTGRES_USER=postgres -POSTGRES_DB=authdb -POSTGRES_PASSWORD=${config.dbPassword} -DB_DATA_PATH=${config.dbDataPath || "/volume1/docker/auth-yes/data"} - -# --- Valkey Configuration --- -VALKEY_HOST=auth-valkey -VALKEY_PORT=6379 -VALKEY_URL=redis://auth-valkey:6379 -`; -} - -export function generateSpireEnv(config: AuthSetupConfig): string { - return `# --- Container Registry --- -REG=${config.reg} -GHCR_REG=${config.ghcrReg || "ghcr.atyg.org"} - -# --- SPIRE Storage & Persistence --- -SPIRE_DATA_PATH=${config.spireDataPath || "/volume1/docker/spire"} -`; -} - -export function generateDockerCompose(): string { - return `version: "3.8" - -services: - auth-api: - image: \${REG}/library/auth-yes-api:latest - env_file: stack.env - labels: - - "traefik.enable=true" - - "traefik.docker.network=traefik-net" - - "traefik.http.routers.auth-api.rule=Host(\`\${SYSTEM_DOMAIN}\`)" - - "traefik.http.routers.auth-api.entrypoints=websecure" - - "traefik.http.routers.auth-api.tls=true" - - "traefik.http.services.auth-api.loadbalancer.server.port=8000" - - "traefik.http.middlewares.auth-forward.forwardauth.address=http://auth-api:8000/api/forward-auth" - - "traefik.http.middlewares.auth-forward.forwardauth.trustForwardHeader=true" - - "traefik.http.middlewares.auth-forward.forwardauth.authResponseHeaders=X-Forwarded-User-Id,X-Forwarded-User,X-Forwarded-Scopes,X-Forwarded-App-Id" - expose: - - "8000" - depends_on: - - auth-db - - auth-valkey - networks: - - default - - traefik-net - volumes: - - spire-socket:/var/run/spire:ro - - auth-db: - image: acr.atyg.org/library/postgres:18-alpine - environment: - - POSTGRES_USER=\${POSTGRES_USER} - - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD} - - POSTGRES_DB=\${POSTGRES_DB} - volumes: - - auth-db-data:/var/lib/postgresql - networks: - - default - - auth-valkey: - image: acr.atyg.org/valkey/valkey:8-alpine - networks: - - default - -volumes: - auth-db-data: - driver: local - driver_opts: - type: none - device: \${DB_DATA_PATH} - o: bind - spire-socket: - name: spire-socket - -networks: - default: - name: auth-internal-net - traefik-net: - external: true -`; -} - -export function generateSpireDockerCompose(): string { - return `version: "3.8" - -services: - spire-server: - image: \${REG}/library/spire-server:latest - container_name: spire-server - hostname: spire-server - networks: - - auth-internal-net - volumes: - - spire-data:/opt/spire - - spire-agent: - image: \${REG}/library/spire-agent:latest - container_name: spire-agent - hostname: spire-agent - pid: host - depends_on: - - spire-server - networks: - - auth-internal-net - volumes: - - spire-data:/opt/spire - - spire-socket:/var/run/spire - - /var/run/docker.sock:/var/run/docker.sock:ro - -volumes: - spire-data: - driver: local - driver_opts: - type: none - device: \${SPIRE_DATA_PATH} - o: bind - spire-socket: - name: spire-socket - -networks: - auth-internal-net: - external: true -`; -} - -export function generateProtobufCompilationCommands(): string[] { - return [ - "deno", - "run", - "-A", - "npm:@bufbuild/buf", - "generate", - "server/auth.proto", - "--template", - '{"version":"v1","plugins":[{"plugin":"buf.build/bufbuild/es:v1.10.0","out":"sdk/gen","opt":"target=ts,import_extension=.ts"},{"plugin":"buf.build/connectrpc/es:v1.4.0","out":"sdk/gen","opt":"target=ts,import_extension=.ts"}]}', - ]; -} - -// SIDE EFFECT: Runs the protoc compilation -export async function executeProtobufCompilation(): Promise { - const commands = generateProtobufCompilationCommands(); - const cmd = new Deno.Command(commands[0], { - args: commands.slice(1), - stdout: "inherit", - stderr: "inherit", - }); - - const { code } = await cmd.output(); - if (code !== 0) { - throw new Error("Failed to compile protobuf definitions."); - } -} - -export async function downloadWorkloadProto(): Promise { - console.log(colors.blue("\nDownloading workload.proto...")); - const res = await fetch( - "https://raw.githubusercontent.com/spiffe/go-spiffe/main/proto/spiffe/workload/workload.proto", - ); - if (!res.ok) { - throw new Error(`Failed to download workload.proto: ${res.statusText}`); - } - const text = await res.text(); - await Deno.mkdir("spire_ffi/proto", { recursive: true }); - await Deno.writeTextFile("spire_ffi/proto/workload.proto", text); - console.log(colors.green("✓ workload.proto downloaded successfully.")); -} - -export function generateBuildCommands(reg: string): string[] { - return [ - `podman build -t ${reg}/library/auth-yes-api:latest -f Dockerfile .`, - `podman push ${reg}/library/auth-yes-api:latest`, - `podman build -t ${reg}/library/spire-server:latest -f spire/Dockerfile.server spire/`, - `podman push ${reg}/library/spire-server:latest`, - `podman build -t ${reg}/library/spire-agent:latest -f spire/Dockerfile.agent spire/`, - `podman push ${reg}/library/spire-agent:latest`, - ]; -} - -// SIDE EFFECT: Executes docker build and push commands to the host system -export async function executeBuildImage(commands: string[]): Promise { - for (const cmd of commands) { - console.log(colors.cyan(`\nExecuting: ${cmd}`)); - const args = cmd.split(" "); - const process = new Deno.Command(args[0], { - args: args.slice(1), - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - }); - - const { code } = await process.output(); - if (code !== 0) { - throw new Error(`Command failed with exit code ${code}: ${cmd}`); - } - } -} - -export async function generateAuthSetupFiles( - config: AuthSetupConfig, -): Promise { - const envContent = generateEnv(config); - await Deno.writeTextFile(ENV_PATH, envContent); - - const spireEnvContent = generateSpireEnv(config); - await Deno.writeTextFile(SPIRE_ENV_PATH, spireEnvContent); - - const composeContent = generateDockerCompose(); - await Deno.writeTextFile(COMPOSE_PATH, composeContent); - - const spireComposeContent = generateSpireDockerCompose(); - await Deno.writeTextFile(SPIRE_COMPOSE_PATH, spireComposeContent); - - console.log( - colors.green( - `\n✓ Successfully generated ${ENV_PATH}, ${SPIRE_ENV_PATH}, ${COMPOSE_PATH}, and ${SPIRE_COMPOSE_PATH}!`, - ), - ); - console.log( - colors.green("Setup complete. You may deploy your stacks by running:\n"), - ); - console.log( - colors.cyan( - "1. Deploy SPIRE Stack:\n" + - " podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d\n\n" + - "2. Deploy Auth-Yes Stack:\n" + - " podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", - ), - ); -} - -export async function handleAuthSetup( - currentConfig: AuthSetupConfig, -): Promise { - console.log( - colors.gray( - "Please provide the following Auth Yes configuration details.\n", - ), - ); - - const reg = await Input.prompt({ - message: "Enter the Primary Container Registry URL:", - default: currentConfig.reg, - }); - - const ghcrReg = await Input.prompt({ - message: "Enter the GHCR Mirror Registry URL (for SPIRE):", - default: currentConfig.ghcrReg || "ghcr.atyg.org", - }); - - const domainName = await Input.prompt({ - message: "Enter the Auth Domain Name:", - hint: "E.g., auth.system.local", - default: currentConfig.domainName, - }); - - const dbDataPath = await Input.prompt({ - message: "Enter the Database Path on the Host:", - default: currentConfig.dbDataPath, - }); - - const spireDataPath = await Input.prompt({ - message: "Enter the SPIRE Path on the Host:", - default: currentConfig.spireDataPath, - }); - - const appSecret = await Secret.prompt({ - message: "Enter the App Secret for the IDP:", - default: currentConfig.appSecret, - minLength: 16, - }); - - const pwdMessage = currentConfig.dbPassword - ? "Enter the PostgreSQL database password: (Leave blank to keep existing password)" - : "Enter the PostgreSQL database password:"; - - const pwdInput = await Secret.prompt({ - message: pwdMessage, - minLength: currentConfig.dbPassword ? 0 : 1, - }); - - const dbPassword = pwdInput === "" && currentConfig.dbPassword !== "" - ? currentConfig.dbPassword - : pwdInput; - - const newConfig: AuthSetupConfig = { - reg, - ghcrReg, - domainName, - dbPassword, - dbDataPath, - spireDataPath, - appSecret, - }; - - await generateAuthSetupFiles(newConfig); - - return newConfig; -} - -export async function handleTestConnection(domainName: string): Promise { - console.log( - colors.bold( - colors.blue(`\n=== Testing Auth Connection (https://${domainName}) ===`), - ), - ); - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - const res1 = await fetch(`https://${domainName}/`, { - signal: controller.signal, - }); - clearTimeout(timeoutId); - - if (res1.status === 502 || res1.status === 503 || res1.status === 504) { - throw new Error(`Gateway error: ${res1.status}`); - } - - if (res1.body) { - await res1.body.cancel(); - } - - console.log( - colors.green( - `✓ Auth service is up and reachable at https://${domainName}/`, - ), - ); - } catch (error) { - console.log( - colors.red( - `✗ Error: Could not reach the auth service at https://${domainName}/`, - ), - ); - if (error instanceof Error) { - console.log(colors.red(` Reason: ${error.message}`)); - } else { - console.log(colors.red(` Reason: ${error}`)); - } - console.log( - colors.red( - " Please verify the container is running and DNS/Traefik is resolving.", - ), - ); - } -} - -async function handleReviewConfigs(): Promise { - const envContent = await Deno.readTextFile(ENV_PATH).catch(() => null); - const composeContent = await Deno.readTextFile(COMPOSE_PATH).catch(() => - null - ); - const spireComposeContent = await Deno.readTextFile(SPIRE_COMPOSE_PATH).catch( - () => null, - ); - - if (!envContent && !composeContent && !spireComposeContent) { - console.log( - colors.red("\n✗ No generated configs found. Run the setup first.\n"), - ); - return; - } - - while (true) { - const action = await Select.prompt({ - message: "Review Generated Configs", - options: [ - { name: "[Show .env]", value: "env" }, - { name: "[Show compose.yml]", value: "compose" }, - { name: "[Show compose.spire.yml]", value: "spire_compose" }, - { name: "[Back to Main Menu]", value: "back" }, - ], - }); - - if (action === "env") { - console.log(colors.bold(colors.blue(`\n=== ${ENV_PATH} ===\n`))); - console.log(envContent || colors.yellow("File not found.")); - console.log(); - } else if (action === "compose") { - console.log(colors.bold(colors.blue(`\n=== ${COMPOSE_PATH} ===\n`))); - console.log(composeContent || colors.yellow("File not found.")); - console.log(); - } else if (action === "spire_compose") { - console.log( - colors.bold(colors.blue(`\n=== ${SPIRE_COMPOSE_PATH} ===\n`)), - ); - console.log(spireComposeContent || colors.yellow("File not found.")); - console.log(); - } else if (action === "back") { - break; - } - } -} - -// SIDE EFFECT: Runs the interactive CLI wizard. -export async function runSetupWizard(): Promise { - console.log(colors.bold(colors.blue("=== Auth Setup Wizard ===\n"))); - - const loadedEnv = await readEnv(); - let currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - - if (Object.keys(loadedEnv).length > 0 && currentConfig.domainName) { - await handleTestConnection(currentConfig.domainName); - console.log(); - } - - while (true) { - const action = await Select.prompt({ - message: "Main Menu", - options: [ - { name: "[Test Auth Connection]", value: "test" }, - { name: "[Configure Auth Yes API]", value: "auth" }, - { name: "[Compile Protobuf Definitions]", value: "compile_proto" }, - { name: "[Review Generated Configs]", value: "review" }, - { name: "[Build and Push Auth Image]", value: "build" }, - { name: "[Exit]", value: "exit" }, - ], - }); - - if (action === "test") { - await handleTestConnection(currentConfig.domainName); - console.log(); - } else if (action === "auth") { - currentConfig = await handleAuthSetup(currentConfig); - } else if (action === "compile_proto") { - try { - await downloadWorkloadProto(); - await executeProtobufCompilation(); - console.log(colors.green("\n✓ Successfully compiled protobufs!\n")); - } catch (error) { - if (error instanceof Error) { - console.log( - colors.red(`\n✗ Protobuf compilation failed: ${error.message}\n`), - ); - } else { - console.log( - colors.red(`\n✗ Protobuf compilation failed: ${error}\n`), - ); - } - } - } else if (action === "review") { - await handleReviewConfigs(); - } else if (action === "build") { - try { - const commands = generateBuildCommands(currentConfig.reg); - await executeBuildImage(commands); - console.log(colors.green("\n✓ Successfully built and pushed image!")); - console.log( - colors.green("You may now deploy your stack by running:\n"), - ); - console.log( - colors.cyan( - "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", - ), - ); - } catch (error) { - if (error instanceof Error) { - console.log( - colors.red(`\n✗ Build process failed: ${error.message}\n`), - ); - } else { - console.log(colors.red(`\n✗ Build process failed: ${error}\n`)); - } - } - } else if (action === "exit") { - console.log(colors.gray("Exiting...\n")); - break; - } - } -} +import { runCli } from "./setup/cli.ts"; if (import.meta.main) { - if (!Deno.stdin.isTerminal() && Deno.args.length === 0) { - console.error( - colors.red( - "Error: Non-interactive environment detected, but no explicit CLI subcommands or --auto flag were provided. Aborting to prevent hangs.", - ), - ); - Deno.exit(1); - } - - const cmd = new Command() - .name("auth-setup") - .description("Auth setup wizard and CLI") - .action(() => { - runSetupWizard(); - }) - .command("auth", "Configure Auth Yes API") - .option("--auto, --headless", "Run in headless mode") - .option("--registry ", "Container Registry URL") - .option("--ghcr-registry ", "GHCR Mirror Registry URL") - .option("--domain ", "Auth Domain Name") - .option("--db-path ", "Database Path on the Host") - .option("--spire-path ", "SPIRE Path on the Host") - .action(async (options) => { - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - - if (options.auto) { - if (!options.registry || !options.domain || !options.dbPath) { - console.error( - colors.red( - "Error: --registry, --domain, and --db-path are required in headless mode.", - ), - ); - Deno.exit(1); - } - const dbPassword = Deno.env.get("POSTGRES_PASSWORD") || - currentConfig.dbPassword; - const appSecret = Deno.env.get("APP_SECRET") || - currentConfig.appSecret; - if (!dbPassword) { - console.error( - colors.red( - "Error: POSTGRES_PASSWORD environment variable is required in headless mode.", - ), - ); - Deno.exit(1); - } - if (!appSecret) { - console.error( - colors.red( - "Error: APP_SECRET environment variable is required in headless mode.", - ), - ); - Deno.exit(1); - } - - const newConfig: AuthSetupConfig = { - reg: options.registry, - ghcrReg: options.ghcrRegistry || currentConfig.ghcrReg || - "ghcr.atyg.org", - domainName: options.domain, - dbPassword, - dbDataPath: options.dbPath || currentConfig.dbDataPath || - "/volume1/docker/auth-yes/data", - spireDataPath: options.spirePath || currentConfig.spireDataPath || - "/volume1/docker/spire", - appSecret, - }; - await generateAuthSetupFiles(newConfig); - } else { - if (options.registry) currentConfig.reg = options.registry; - if (options.ghcrRegistry) currentConfig.ghcrReg = options.ghcrRegistry; - if (options.domain) currentConfig.domainName = options.domain; - if (options.dbPath) currentConfig.dbDataPath = options.dbPath; - if (options.spirePath) currentConfig.spireDataPath = options.spirePath; - await handleAuthSetup(currentConfig); - } - }) - .command("test", "Test Auth Connection") - .action(async () => { - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - await handleTestConnection(currentConfig.domainName); - }) - .command("compile_proto", "Compile Protobuf Definitions") - .action(async () => { - try { - await downloadWorkloadProto(); - await executeProtobufCompilation(); - console.log(colors.green("\n✓ Successfully compiled protobufs!\n")); - } catch (error) { - if (error instanceof Error) { - console.log( - colors.red(`\n✗ Protobuf compilation failed: ${error.message}\n`), - ); - } else { - console.log( - colors.red(`\n✗ Protobuf compilation failed: ${error}\n`), - ); - } - Deno.exit(1); - } - }) - .command("build", "Build and Push Auth Image") - .action(async () => { - try { - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - const commands = generateBuildCommands(currentConfig.reg); - await executeBuildImage(commands); - console.log( - colors.green("\n✓ Successfully built and pushed auth image!"), - ); - console.log( - colors.green("You may now deploy your stack by running:\n"), - ); - console.log( - colors.cyan( - "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", - ), - ); - } catch (error) { - if (error instanceof Error) { - console.log( - colors.red(`\n✗ Build process failed: ${error.message}\n`), - ); - } else { - console.log(colors.red(`\n✗ Build process failed: ${error}\n`)); - } - Deno.exit(1); - } - }) - .command("dump_compose", "Output all generated Compose YAML configurations") - .action(() => { - console.log( - colors.bold( - colors.blue( - "\n================================================================", - ), - ), - ); - console.log( - colors.bold( - colors.green(" STACK 1: Auth-Yes Stack (infra/compose.yml)"), - ), - ); - console.log( - colors.bold( - colors.blue( - "================================================================\n", - ), - ), - ); - console.log(generateDockerCompose()); - console.log( - colors.bold( - colors.blue( - "================================================================", - ), - ), - ); - console.log( - colors.bold( - colors.green( - " STACK 2: Standalone SPIRE Stack (infra/compose.spire.yml)", - ), - ), - ); - console.log( - colors.bold( - colors.blue( - "================================================================\n", - ), - ), - ); - console.log(generateSpireDockerCompose()); - }) - .command( - "dump_env", - "Output current environment configuration (.env and .env.spire)", - ) - .action(async () => { - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - console.log( - colors.bold( - colors.blue( - "\n================================================================", - ), - ), - ); - console.log( - colors.bold( - colors.green(" STACK 1: Auth-Yes Environment (infra/stack.env)"), - ), - ); - console.log( - colors.bold( - colors.blue( - "================================================================\n", - ), - ), - ); - console.log(generateEnv(currentConfig)); - console.log( - colors.bold( - colors.blue( - "================================================================", - ), - ), - ); - console.log( - colors.bold( - colors.green( - " STACK 2: SPIRE Stack Environment (infra/.env.spire)", - ), - ), - ); - console.log( - colors.bold( - colors.blue( - "================================================================\n", - ), - ), - ); - console.log(generateSpireEnv(currentConfig)); - }) - .command("secrets", "Inspect secrets status and persistence paths") - .action(async () => { - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - console.log( - colors.bold( - colors.blue( - "\n=== Auth-Yes Secrets & Persistence Topology ===\n", - ), - ), - ); - console.log( - `${ - colors.cyan("Database Persistence Path:") - } ${currentConfig.dbDataPath}`, - ); - console.log( - `${ - colors.cyan("SPIRE Persistence Path:") - } ${currentConfig.spireDataPath}`, - ); - console.log( - `${ - colors.cyan("Local Artifact Files:") - } ${ENV_PATH}, ${SPIRE_ENV_PATH}, ${COMPOSE_PATH}, ${SPIRE_COMPOSE_PATH}`, - ); - console.log( - `${colors.cyan("PostgreSQL Password:")} ${ - currentConfig.dbPassword - ? colors.green( - "✓ Configured (len: " + currentConfig.dbPassword.length + ")", - ) - : colors.red("✗ Missing") - }`, - ); - console.log( - `${colors.cyan("Application Secret:")} ${ - currentConfig.appSecret - ? colors.green( - "✓ Configured (len: " + currentConfig.appSecret.length + ")", - ) - : colors.red("✗ Missing") - }`, - ); - console.log( - colors.gray( - "\nTip: To dump raw environment variables, run: deno task setup dump_env\n", - ), - ); - }) - .command( - "release", - "Run complete build & release pipeline with copy-paste deployment instructions", - ) - .action(async () => { - console.log( - colors.bold( - colors.blue( - "\n================================================================", - ), - ), - ); - console.log( - colors.bold( - colors.green( - " Auth-Yes Infrastructure Build & Release Pipeline", - ), - ), - ); - console.log( - colors.bold( - colors.blue( - "================================================================\n", - ), - ), - ); - - // 1. Quality Gates - console.log(colors.cyan("[1/4] Running Quality Gates & Test Suite...")); - const testCmd = new Deno.Command("deno", { - args: ["task", "test"], - stdout: "inherit", - stderr: "inherit", - }); - const testRes = await testCmd.output(); - if (testRes.code !== 0) { - console.error( - colors.red("\n✗ Quality gates failed. Aborting release."), - ); - Deno.exit(1); - } - - // 2. Compile Protobufs - console.log( - colors.cyan("\n[2/4] Downloading & Compiling Protobufs..."), - ); - await downloadWorkloadProto(); - await executeProtobufCompilation(); - - // 3. Build & Push Images - console.log( - colors.cyan( - "\n[3/4] Building and Pushing Custom Container Images...", - ), - ); - const loadedEnv = await readEnv(); - const currentConfig: AuthSetupConfig = { - ...DEFAULT_AUTH_CONFIG, - ...loadedEnv, - }; - const commands = generateBuildCommands(currentConfig.reg); - await executeBuildImage(commands); - console.log( - colors.green( - "\n✓ Successfully built and pushed all stack images to " + - currentConfig.reg + "!", - ), - ); - - // 4. Output Copy-Paste Guides - console.log( - colors.cyan( - "\n[4/4] Release Complete. Deployment & Configuration Guides:\n", - ), - ); - - console.log( - colors.bold( - colors.yellow( - "┌──────────────────────────────────────────────────────────────┐", - ), - ), - ); - console.log( - colors.bold( - colors.yellow( - "│ A. FOR NEW SYSTEM INSTALLATIONS │", - ), - ), - ); - console.log( - colors.bold( - colors.yellow( - "└──────────────────────────────────────────────────────────────┘", - ), - ), - ); - console.log( - colors.gray("# 1. Inspect environment variables and secrets:"), - ); - console.log(colors.cyan("deno task setup dump_env")); - console.log( - colors.gray("\n# 2. Inspect generated Docker Compose files:"), - ); - console.log(colors.cyan("deno task setup dump_compose")); - console.log( - colors.gray("\n# 3. Create persistent storage paths on host:"), - ); - console.log( - colors.cyan( - `mkdir -p ${currentConfig.spireDataPath} ${currentConfig.dbDataPath}`, - ), - ); - console.log(colors.gray("\n# 4. Deploy fresh stacks:")); - console.log( - colors.cyan( - "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d", - ), - ); - console.log( - colors.cyan( - "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d", - ), - ); - - console.log( - colors.bold( - colors.yellow( - "\n┌──────────────────────────────────────────────────────────────┐", - ), - ), - ); - console.log( - colors.bold( - colors.yellow( - "│ B. FOR EXISTING SYSTEM UPDATES (ROLLING RESTART) │", - ), - ), - ); - console.log( - colors.bold( - colors.yellow( - "└──────────────────────────────────────────────────────────────┘", - ), - ), - ); - console.log( - colors.gray( - "# Pull newly pushed custom images and restart containers:", - ), - ); - console.log( - colors.cyan( - "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml pull", - ), - ); - console.log( - colors.cyan( - "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d\n", - ), - ); - console.log( - colors.cyan( - "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml pull", - ), - ); - console.log( - colors.cyan( - "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", - ), - ); - }); - - await cmd.parse(Deno.args); + await runCli(); } diff --git a/infra/setup/build.ts b/infra/setup/build.ts new file mode 100644 index 0000000..b4f14ae --- /dev/null +++ b/infra/setup/build.ts @@ -0,0 +1,73 @@ +import * as colors from "jsr:@std/fmt@0.225.2/colors"; + +export function generateProtobufCompilationCommands(): string[] { + return [ + "deno", + "run", + "-A", + "npm:@bufbuild/buf", + "generate", + "server/auth.proto", + "--template", + '{"version":"v1","plugins":[{"plugin":"buf.build/bufbuild/es:v1.10.0","out":"sdk/gen","opt":"target=ts,import_extension=.ts"},{"plugin":"buf.build/connectrpc/es:v1.4.0","out":"sdk/gen","opt":"target=ts,import_extension=.ts"}]}', + ]; +} + +// SIDE EFFECT: Runs the protoc compilation +export async function executeProtobufCompilation(): Promise { + const commands = generateProtobufCompilationCommands(); + const cmd = new Deno.Command(commands[0], { + args: commands.slice(1), + stdout: "inherit", + stderr: "inherit", + }); + + const { code } = await cmd.output(); + if (code !== 0) { + throw new Error("Failed to compile protobuf definitions."); + } +} + +export async function downloadWorkloadProto(): Promise { + console.log(colors.blue("\nDownloading workload.proto...")); + const res = await fetch( + "https://raw.githubusercontent.com/spiffe/go-spiffe/main/proto/spiffe/workload/workload.proto", + ); + if (!res.ok) { + throw new Error(`Failed to download workload.proto: ${res.statusText}`); + } + const text = await res.text(); + await Deno.mkdir("spire_ffi/proto", { recursive: true }); + await Deno.writeTextFile("spire_ffi/proto/workload.proto", text); + console.log(colors.green("✓ workload.proto downloaded successfully.")); +} + +export function generateBuildCommands(reg: string): string[] { + return [ + `podman build -t ${reg}/library/auth-yes-api:latest -f Dockerfile .`, + `podman push ${reg}/library/auth-yes-api:latest`, + `podman build -t ${reg}/library/spire-server:latest -f spire/Dockerfile.server spire/`, + `podman push ${reg}/library/spire-server:latest`, + `podman build -t ${reg}/library/spire-agent:latest -f spire/Dockerfile.agent spire/`, + `podman push ${reg}/library/spire-agent:latest`, + ]; +} + +// SIDE EFFECT: Executes docker build and push commands to the host system +export async function executeBuildImage(commands: string[]): Promise { + for (const cmd of commands) { + console.log(colors.cyan(`\nExecuting: ${cmd}`)); + const args = cmd.split(" "); + const process = new Deno.Command(args[0], { + args: args.slice(1), + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }); + + const { code } = await process.output(); + if (code !== 0) { + throw new Error(`Command failed with exit code ${code}: ${cmd}`); + } + } +} diff --git a/infra/setup/cli.ts b/infra/setup/cli.ts new file mode 100644 index 0000000..0d193f1 --- /dev/null +++ b/infra/setup/cli.ts @@ -0,0 +1,771 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import { Input, Secret, Select } from "jsr:@cliffy/prompt@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { + generateDockerCompose, + generateSpireDockerCompose, +} from "./compose.ts"; +import { + AuthSetupConfig, + COMPOSE_PATH, + DEFAULT_AUTH_CONFIG, + ENV_PATH, + generateEnv, + generateSpireEnv, + readEnv, + SPIRE_COMPOSE_PATH, + SPIRE_ENV_PATH, +} from "./env.ts"; +import { + downloadWorkloadProto, + executeBuildImage, + executeProtobufCompilation, + generateBuildCommands, +} from "./build.ts"; + +export async function generateAuthSetupFiles( + config: AuthSetupConfig, +): Promise { + const envContent = generateEnv(config); + await Deno.writeTextFile(ENV_PATH, envContent); + + const spireEnvContent = generateSpireEnv(config); + await Deno.writeTextFile(SPIRE_ENV_PATH, spireEnvContent); + + const composeContent = generateDockerCompose(); + await Deno.writeTextFile(COMPOSE_PATH, composeContent); + + const spireComposeContent = generateSpireDockerCompose(); + await Deno.writeTextFile(SPIRE_COMPOSE_PATH, spireComposeContent); + + console.log( + colors.green( + `\n✓ Successfully generated ${ENV_PATH}, ${SPIRE_ENV_PATH}, ${COMPOSE_PATH}, and ${SPIRE_COMPOSE_PATH}!`, + ), + ); + console.log( + colors.green("Setup complete. You may deploy your stacks by running:\n"), + ); + console.log( + colors.cyan( + "1. Deploy SPIRE Stack:\n" + + " podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d\n\n" + + "2. Deploy Auth-Yes Stack:\n" + + " podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", + ), + ); +} + +export async function handleAuthSetup( + currentConfig: AuthSetupConfig, +): Promise { + console.log( + colors.gray( + "Please provide the following Auth Yes configuration details.\n", + ), + ); + + const reg = await Input.prompt({ + message: "Enter the Primary Container Registry URL:", + default: currentConfig.reg, + }); + + const ghcrReg = await Input.prompt({ + message: "Enter the GHCR Mirror Registry URL (for SPIRE):", + default: currentConfig.ghcrReg || "ghcr.atyg.org", + }); + + const domainName = await Input.prompt({ + message: "Enter the Auth Domain Name:", + hint: "E.g., auth.system.local", + default: currentConfig.domainName, + }); + + const dbDataPath = await Input.prompt({ + message: "Enter the Database Path on the Host:", + default: currentConfig.dbDataPath, + }); + + const spireDataPath = await Input.prompt({ + message: "Enter the SPIRE Path on the Host:", + default: currentConfig.spireDataPath, + }); + + const appSecret = await Secret.prompt({ + message: "Enter the App Secret for the IDP:", + default: currentConfig.appSecret, + minLength: 16, + }); + + const pwdMessage = currentConfig.dbPassword + ? "Enter the PostgreSQL database password: (Leave blank to keep existing password)" + : "Enter the PostgreSQL database password:"; + + const pwdInput = await Secret.prompt({ + message: pwdMessage, + minLength: currentConfig.dbPassword ? 0 : 1, + }); + + const dbPassword = pwdInput === "" && currentConfig.dbPassword !== "" + ? currentConfig.dbPassword + : pwdInput; + + const newConfig: AuthSetupConfig = { + reg, + ghcrReg, + domainName, + dbPassword, + dbDataPath, + spireDataPath, + appSecret, + }; + + await generateAuthSetupFiles(newConfig); + + return newConfig; +} + +export async function handleTestConnection(domainName: string): Promise { + console.log( + colors.bold( + colors.blue(`\n=== Testing Auth Connection (https://${domainName}) ===`), + ), + ); + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const res1 = await fetch(`https://${domainName}/`, { + signal: controller.signal, + }); + clearTimeout(timeoutId); + + if (res1.status === 502 || res1.status === 503 || res1.status === 504) { + throw new Error(`Gateway error: ${res1.status}`); + } + + if (res1.body) { + await res1.body.cancel(); + } + + console.log( + colors.green( + `✓ Auth service is up and reachable at https://${domainName}/`, + ), + ); + } catch (error) { + console.log( + colors.red( + `✗ Error: Could not reach the auth service at https://${domainName}/`, + ), + ); + if (error instanceof Error) { + console.log(colors.red(` Reason: ${error.message}`)); + } else { + console.log(colors.red(` Reason: ${error}`)); + } + console.log( + colors.red( + " Please verify the container is running and DNS/Traefik is resolving.", + ), + ); + } +} + +async function handleReviewConfigs(): Promise { + const envContent = await Deno.readTextFile(ENV_PATH).catch(() => null); + const composeContent = await Deno.readTextFile(COMPOSE_PATH).catch(() => + null + ); + const spireComposeContent = await Deno.readTextFile(SPIRE_COMPOSE_PATH).catch( + () => null, + ); + + if (!envContent && !composeContent && !spireComposeContent) { + console.log( + colors.red("\n✗ No generated configs found. Run the setup first.\n"), + ); + return; + } + + while (true) { + const action = await Select.prompt({ + message: "Review Generated Configs", + options: [ + { name: "[Show .env]", value: "env" }, + { name: "[Show compose.yml]", value: "compose" }, + { name: "[Show compose.spire.yml]", value: "spire_compose" }, + { name: "[Back to Main Menu]", value: "back" }, + ], + }); + + if (action === "env") { + console.log(colors.bold(colors.blue(`\n=== ${ENV_PATH} ===\n`))); + console.log(envContent || colors.yellow("File not found.")); + console.log(); + } else if (action === "compose") { + console.log(colors.bold(colors.blue(`\n=== ${COMPOSE_PATH} ===\n`))); + console.log(composeContent || colors.yellow("File not found.")); + console.log(); + } else if (action === "spire_compose") { + console.log( + colors.bold(colors.blue(`\n=== ${SPIRE_COMPOSE_PATH} ===\n`)), + ); + console.log(spireComposeContent || colors.yellow("File not found.")); + console.log(); + } else if (action === "back") { + break; + } + } +} + +// SIDE EFFECT: Runs the interactive CLI wizard. +export async function runSetupWizard(): Promise { + console.log(colors.bold(colors.blue("=== Auth Setup Wizard ===\n"))); + + const loadedEnv = await readEnv(); + let currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + + if (Object.keys(loadedEnv).length > 0 && currentConfig.domainName) { + await handleTestConnection(currentConfig.domainName); + console.log(); + } + + while (true) { + const action = await Select.prompt({ + message: "Main Menu", + options: [ + { name: "[Test Auth Connection]", value: "test" }, + { name: "[Configure Auth Yes API]", value: "auth" }, + { name: "[Compile Protobuf Definitions]", value: "compile_proto" }, + { name: "[Review Generated Configs]", value: "review" }, + { name: "[Build and Push Auth Image]", value: "build" }, + { name: "[Exit]", value: "exit" }, + ], + }); + + if (action === "test") { + await handleTestConnection(currentConfig.domainName); + console.log(); + } else if (action === "auth") { + currentConfig = await handleAuthSetup(currentConfig); + } else if (action === "compile_proto") { + try { + await downloadWorkloadProto(); + await executeProtobufCompilation(); + console.log(colors.green("\n✓ Successfully compiled protobufs!\n")); + } catch (error) { + if (error instanceof Error) { + console.log( + colors.red(`\n✗ Protobuf compilation failed: ${error.message}\n`), + ); + } else { + console.log( + colors.red(`\n✗ Protobuf compilation failed: ${error}\n`), + ); + } + } + } else if (action === "review") { + await handleReviewConfigs(); + } else if (action === "build") { + try { + const commands = generateBuildCommands(currentConfig.reg); + await executeBuildImage(commands); + console.log(colors.green("\n✓ Successfully built and pushed image!")); + console.log( + colors.green("You may now deploy your stack by running:\n"), + ); + console.log( + colors.cyan( + "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", + ), + ); + } catch (error) { + if (error instanceof Error) { + console.log( + colors.red(`\n✗ Build process failed: ${error.message}\n`), + ); + } else { + console.log(colors.red(`\n✗ Build process failed: ${error}\n`)); + } + } + } else if (action === "exit") { + console.log(colors.gray("Exiting...\n")); + break; + } + } +} + +export async function runCli(args = Deno.args): Promise { + if (import.meta.main || args.length === 0) { + if (!Deno.stdin.isTerminal() && args.length === 0) { + console.error( + colors.red( + "Error: Non-interactive environment detected, but no explicit CLI subcommands or --auto flag were provided. Aborting to prevent hangs.", + ), + ); + Deno.exit(1); + } + } + + const cmd = new Command() + .name("auth-setup") + .description("Auth setup wizard and CLI") + .action(() => { + runSetupWizard(); + }) + .command("auth", "Configure Auth Yes API") + .option("--auto, --headless", "Run in headless mode") + .option("--registry ", "Container Registry URL") + .option("--ghcr-registry ", "GHCR Mirror Registry URL") + .option("--domain ", "Auth Domain Name") + .option("--db-path ", "Database Path on the Host") + .option("--spire-path ", "SPIRE Path on the Host") + .action(async (options) => { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + + if (options.auto) { + if (!options.registry || !options.domain || !options.dbPath) { + console.error( + colors.red( + "Error: --registry, --domain, and --db-path are required in headless mode.", + ), + ); + Deno.exit(1); + } + const dbPassword = Deno.env.get("POSTGRES_PASSWORD") || + currentConfig.dbPassword; + const appSecret = Deno.env.get("APP_SECRET") || + currentConfig.appSecret; + if (!dbPassword) { + console.error( + colors.red( + "Error: POSTGRES_PASSWORD environment variable is required in headless mode.", + ), + ); + Deno.exit(1); + } + if (!appSecret) { + console.error( + colors.red( + "Error: APP_SECRET environment variable is required in headless mode.", + ), + ); + Deno.exit(1); + } + + const newConfig: AuthSetupConfig = { + reg: options.registry, + ghcrReg: options.ghcrRegistry || currentConfig.ghcrReg || + "ghcr.atyg.org", + domainName: options.domain, + dbPassword, + dbDataPath: options.dbPath || currentConfig.dbDataPath || + "/volume1/docker/auth-yes/data", + spireDataPath: options.spirePath || currentConfig.spireDataPath || + "/volume1/docker/spire", + appSecret, + }; + await generateAuthSetupFiles(newConfig); + } else { + if (options.registry) currentConfig.reg = options.registry; + if (options.ghcrRegistry) currentConfig.ghcrReg = options.ghcrRegistry; + if (options.domain) currentConfig.domainName = options.domain; + if (options.dbPath) currentConfig.dbDataPath = options.dbPath; + if (options.spirePath) currentConfig.spireDataPath = options.spirePath; + await handleAuthSetup(currentConfig); + } + }) + .command("test", "Test Auth Connection") + .action(async () => { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + await handleTestConnection(currentConfig.domainName); + }) + .command("compile_proto", "Compile Protobuf Definitions") + .action(async () => { + try { + await downloadWorkloadProto(); + await executeProtobufCompilation(); + console.log(colors.green("\n✓ Successfully compiled protobufs!\n")); + } catch (error) { + if (error instanceof Error) { + console.log( + colors.red(`\n✗ Protobuf compilation failed: ${error.message}\n`), + ); + } else { + console.log( + colors.red(`\n✗ Protobuf compilation failed: ${error}\n`), + ); + } + Deno.exit(1); + } + }) + .command("build", "Build and Push Auth Image") + .action(async () => { + try { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + const commands = generateBuildCommands(currentConfig.reg); + await executeBuildImage(commands); + console.log( + colors.green("\n✓ Successfully built and pushed auth image!"), + ); + console.log( + colors.green("You may now deploy your stack by running:\n"), + ); + console.log( + colors.cyan( + "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", + ), + ); + } catch (error) { + if (error instanceof Error) { + console.log( + colors.red(`\n✗ Build process failed: ${error.message}\n`), + ); + } else { + console.log(colors.red(`\n✗ Build process failed: ${error}\n`)); + } + Deno.exit(1); + } + }) + .command("dump_compose", "Output all generated Compose YAML configurations") + .action(() => { + console.log( + colors.bold( + colors.blue( + "\n================================================================", + ), + ), + ); + console.log( + colors.bold( + colors.green(" STACK 1: Auth-Yes Stack (infra/compose.yml)"), + ), + ); + console.log( + colors.bold( + colors.blue( + "================================================================\n", + ), + ), + ); + console.log(generateDockerCompose()); + console.log( + colors.bold( + colors.blue( + "================================================================", + ), + ), + ); + console.log( + colors.bold( + colors.green( + " STACK 2: Standalone SPIRE Stack (infra/compose.spire.yml)", + ), + ), + ); + console.log( + colors.bold( + colors.blue( + "================================================================\n", + ), + ), + ); + console.log(generateSpireDockerCompose()); + }) + .command( + "dump_env", + "Output current environment configuration (.env and .env.spire)", + ) + .action(async () => { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + console.log( + colors.bold( + colors.blue( + "\n================================================================", + ), + ), + ); + console.log( + colors.bold( + colors.green(" STACK 1: Auth-Yes Environment (infra/stack.env)"), + ), + ); + console.log( + colors.bold( + colors.blue( + "================================================================\n", + ), + ), + ); + console.log(generateEnv(currentConfig)); + console.log( + colors.bold( + colors.blue( + "================================================================", + ), + ), + ); + console.log( + colors.bold( + colors.green( + " STACK 2: SPIRE Stack Environment (infra/.env.spire)", + ), + ), + ); + console.log( + colors.bold( + colors.blue( + "================================================================\n", + ), + ), + ); + console.log(generateSpireEnv(currentConfig)); + }) + .command("secrets", "Inspect secrets status and persistence paths") + .action(async () => { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + console.log( + colors.bold( + colors.blue( + "\n=== Auth-Yes Secrets & Persistence Topology ===\n", + ), + ), + ); + console.log( + `${ + colors.cyan("Database Persistence Path:") + } ${currentConfig.dbDataPath}`, + ); + console.log( + `${ + colors.cyan("SPIRE Persistence Path:") + } ${currentConfig.spireDataPath}`, + ); + console.log( + `${ + colors.cyan("Local Artifact Files:") + } ${ENV_PATH}, ${SPIRE_ENV_PATH}, ${COMPOSE_PATH}, ${SPIRE_COMPOSE_PATH}`, + ); + console.log( + `${colors.cyan("PostgreSQL Password:")} ${ + currentConfig.dbPassword + ? colors.green( + "✓ Configured (len: " + currentConfig.dbPassword.length + ")", + ) + : colors.red("✗ Missing") + }`, + ); + console.log( + `${colors.cyan("Application Secret:")} ${ + currentConfig.appSecret + ? colors.green( + "✓ Configured (len: " + currentConfig.appSecret.length + ")", + ) + : colors.red("✗ Missing") + }`, + ); + console.log( + colors.gray( + "\nTip: To dump raw environment variables, run: deno task setup dump_env\n", + ), + ); + }) + .command( + "release", + "Run complete build & release pipeline with copy-paste deployment instructions", + ) + .action(async () => { + console.log( + colors.bold( + colors.blue( + "\n================================================================", + ), + ), + ); + console.log( + colors.bold( + colors.green( + " Auth-Yes Infrastructure Build & Release Pipeline", + ), + ), + ); + console.log( + colors.bold( + colors.blue( + "================================================================\n", + ), + ), + ); + + // 1. Quality Gates + console.log(colors.cyan("[1/4] Running Quality Gates & Test Suite...")); + const testCmd = new Deno.Command("deno", { + args: ["task", "test"], + stdout: "inherit", + stderr: "inherit", + }); + const testRes = await testCmd.output(); + if (testRes.code !== 0) { + console.error( + colors.red("\n✗ Quality gates failed. Aborting release."), + ); + Deno.exit(1); + } + + // 2. Compile Protobufs + console.log( + colors.cyan("\n[2/4] Downloading & Compiling Protobufs..."), + ); + await downloadWorkloadProto(); + await executeProtobufCompilation(); + + // 3. Build & Push Images + console.log( + colors.cyan( + "\n[3/4] Building and Pushing Custom Container Images...", + ), + ); + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + const commands = generateBuildCommands(currentConfig.reg); + await executeBuildImage(commands); + console.log( + colors.green( + "\n✓ Successfully built and pushed all stack images to " + + currentConfig.reg + "!", + ), + ); + + // 4. Output Copy-Paste Guides + console.log( + colors.cyan( + "\n[4/4] Release Complete. Deployment & Configuration Guides:\n", + ), + ); + + console.log( + colors.bold( + colors.yellow( + "┌──────────────────────────────────────────────────────────────┐", + ), + ), + ); + console.log( + colors.bold( + colors.yellow( + "│ A. FOR NEW SYSTEM INSTALLATIONS │", + ), + ), + ); + console.log( + colors.bold( + colors.yellow( + "└──────────────────────────────────────────────────────────────┘", + ), + ), + ); + console.log( + colors.gray("# 1. Inspect environment variables and secrets:"), + ); + console.log(colors.cyan("deno task setup dump_env")); + console.log( + colors.gray("\n# 2. Inspect generated Docker Compose files:"), + ); + console.log(colors.cyan("deno task setup dump_compose")); + console.log( + colors.gray("\n# 3. Create persistent storage paths on host:"), + ); + console.log( + colors.cyan( + `mkdir -p ${currentConfig.spireDataPath} ${currentConfig.dbDataPath}`, + ), + ); + console.log(colors.gray("\n# 4. Deploy fresh stacks:")); + console.log( + colors.cyan( + "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d", + ), + ); + console.log( + colors.cyan( + "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d", + ), + ); + + console.log( + colors.bold( + colors.yellow( + "\n┌──────────────────────────────────────────────────────────────┐", + ), + ), + ); + console.log( + colors.bold( + colors.yellow( + "│ B. FOR EXISTING SYSTEM UPDATES (ROLLING RESTART) │", + ), + ), + ); + console.log( + colors.bold( + colors.yellow( + "└──────────────────────────────────────────────────────────────┘", + ), + ), + ); + console.log( + colors.gray( + "# Pull newly pushed custom images and restart containers:", + ), + ); + console.log( + colors.cyan( + "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml pull", + ), + ); + console.log( + colors.cyan( + "podman-compose --project-name spire --env-file infra/.env.spire -f infra/compose.spire.yml up -d\n", + ), + ); + console.log( + colors.cyan( + "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml pull", + ), + ); + console.log( + colors.cyan( + "podman-compose --project-name auth-yes --env-file infra/stack.env -f infra/compose.yml up -d\n", + ), + ); + }); + + await cmd.parse(args); +} diff --git a/infra/setup/compose.ts b/infra/setup/compose.ts new file mode 100644 index 0000000..36fe196 --- /dev/null +++ b/infra/setup/compose.ts @@ -0,0 +1,104 @@ +export function generateDockerCompose(): string { + return `version: "3.8" + +services: + auth-api: + image: \${REG}/library/auth-yes-api:latest + env_file: stack.env + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-net" + - "traefik.http.routers.auth-api.rule=Host(\`\${SYSTEM_DOMAIN}\`)" + - "traefik.http.routers.auth-api.entrypoints=websecure" + - "traefik.http.routers.auth-api.tls=true" + - "traefik.http.services.auth-api.loadbalancer.server.port=8000" + - "traefik.http.middlewares.auth-forward.forwardauth.address=http://auth-api:8000/api/forward-auth" + - "traefik.http.middlewares.auth-forward.forwardauth.trustForwardHeader=true" + - "traefik.http.middlewares.auth-forward.forwardauth.authResponseHeaders=X-Forwarded-User-Id,X-Forwarded-User,X-Forwarded-Scopes,X-Forwarded-App-Id" + expose: + - "8000" + depends_on: + - auth-db + - auth-valkey + networks: + - default + - traefik-net + volumes: + - spire-socket:/var/run/spire:ro + + auth-db: + image: acr.atyg.org/library/postgres:18-alpine + environment: + - POSTGRES_USER=\${POSTGRES_USER} + - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD} + - POSTGRES_DB=\${POSTGRES_DB} + volumes: + - auth-db-data:/var/lib/postgresql + networks: + - default + + auth-valkey: + image: acr.atyg.org/valkey/valkey:8-alpine + networks: + - default + +volumes: + auth-db-data: + driver: local + driver_opts: + type: none + device: \${DB_DATA_PATH} + o: bind + spire-socket: + name: spire-socket + +networks: + default: + name: auth-internal-net + traefik-net: + external: true +`; +} + +export function generateSpireDockerCompose(): string { + return `version: "3.8" + +services: + spire-server: + image: \${REG}/library/spire-server:latest + container_name: spire-server + hostname: spire-server + networks: + - auth-internal-net + volumes: + - spire-data:/opt/spire + + spire-agent: + image: \${REG}/library/spire-agent:latest + container_name: spire-agent + hostname: spire-agent + pid: host + depends_on: + - spire-server + networks: + - auth-internal-net + volumes: + - spire-data:/opt/spire + - spire-socket:/var/run/spire + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + spire-data: + driver: local + driver_opts: + type: none + device: \${SPIRE_DATA_PATH} + o: bind + spire-socket: + name: spire-socket + +networks: + auth-internal-net: + external: true +`; +} diff --git a/infra/setup/env.ts b/infra/setup/env.ts new file mode 100644 index 0000000..8720288 --- /dev/null +++ b/infra/setup/env.ts @@ -0,0 +1,95 @@ +import * as path from "jsr:@std/path@0.225.2"; + +export const ENV_PATH = path.join("infra", "stack.env"); +export const SPIRE_ENV_PATH = path.join("infra", ".env.spire"); +export const COMPOSE_PATH = path.join("infra", "compose.yml"); +export const SPIRE_COMPOSE_PATH = path.join("infra", "compose.spire.yml"); + +export interface AuthSetupConfig { + reg: string; + ghcrReg: string; + domainName: string; + dbPassword: string; + dbDataPath: string; + spireDataPath: string; + appSecret: string; +} + +export const DEFAULT_AUTH_CONFIG: AuthSetupConfig = { + reg: "quay.atyg.org", + ghcrReg: "ghcr.atyg.org", + domainName: "auth.system.local", + dbPassword: "", + dbDataPath: "/volume1/docker/auth-yes/data", + spireDataPath: "/volume1/docker/spire", + appSecret: "", +}; + +export async function readEnv(): Promise> { + const config: Partial = {}; + for ( + const filePath of [ + ENV_PATH, + path.join("infra", ".env"), + SPIRE_ENV_PATH, + path.join("infra", "stack.env.spire"), + ] + ) { + try { + const text = await Deno.readTextFile(filePath); + for (const line of text.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const [key, ...rest] = trimmed.split("="); + const val = rest.join("=").trim(); + if (key === "REG") config.reg = val; + if (key === "GHCR_REG") config.ghcrReg = val; + if (key === "SYSTEM_DOMAIN") config.domainName = val; + if (key === "POSTGRES_PASSWORD") config.dbPassword = val; + if (key === "DB_DATA_PATH") config.dbDataPath = val; + if (key === "SPIRE_DATA_PATH") config.spireDataPath = val; + if (key === "APP_SECRET") config.appSecret = val; + } + } catch (_e) { + // Ignore and check next + } + } + return config; +} + +export function generateEnv(config: AuthSetupConfig): string { + return `# --- Container Registry --- +REG=${config.reg} + +# --- Network & Routing --- +SYSTEM_DOMAIN=${config.domainName} +RP_ID=${config.domainName} +ORIGIN=https://${config.domainName} + +# --- Identity Provider Internal App Secret --- +APP_SECRET=${config.appSecret} + +# --- Database Configuration --- +POSTGRES_HOST=auth-db +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_DB=authdb +POSTGRES_PASSWORD=${config.dbPassword} +DB_DATA_PATH=${config.dbDataPath || "/volume1/docker/auth-yes/data"} + +# --- Valkey Configuration --- +VALKEY_HOST=auth-valkey +VALKEY_PORT=6379 +VALKEY_URL=redis://auth-valkey:6379 +`; +} + +export function generateSpireEnv(config: AuthSetupConfig): string { + return `# --- Container Registry --- +REG=${config.reg} +GHCR_REG=${config.ghcrReg || "ghcr.atyg.org"} + +# --- SPIRE Storage & Persistence --- +SPIRE_DATA_PATH=${config.spireDataPath || "/volume1/docker/spire"} +`; +} diff --git a/tasks/new/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.md b/tasks/complete/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.md similarity index 100% rename from tasks/new/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.md rename to tasks/complete/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.md