From 752bfcf03e26c38e5bc0a1a7054ef2eefc17cd8b 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 04:37:41 +0000 Subject: [PATCH] Refactor `infra/setup/cli.ts` into smaller command and prompt modules Decomposes the monolith `infra/setup/cli.ts` into clean `infra/setup/prompts/` and `infra/setup/commands/` directories while adhering to Cliffy idiomatic modularity. Validated via `deno check`, tests, and format. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com> --- infra/setup/cli.ts | 764 +----------------- infra/setup/commands/auth.ts | 71 ++ infra/setup/commands/build.ts | 235 ++++++ infra/setup/commands/compose.ts | 112 +++ infra/setup/commands/secrets.ts | 66 ++ infra/setup/commands/test.ts | 61 ++ infra/setup/prompts/auth.ts | 134 +++ infra/setup/prompts/wizard.ts | 145 ++++ ...nolith-decomposition-roadmap-1845.ph4.1.md | 0 9 files changed, 842 insertions(+), 746 deletions(-) create mode 100644 infra/setup/commands/auth.ts create mode 100644 infra/setup/commands/build.ts create mode 100644 infra/setup/commands/compose.ts create mode 100644 infra/setup/commands/secrets.ts create mode 100644 infra/setup/commands/test.ts create mode 100644 infra/setup/prompts/auth.ts create mode 100644 infra/setup/prompts/wizard.ts rename tasks/{new => complete}/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.1.md (100%) diff --git a/infra/setup/cli.ts b/infra/setup/cli.ts index 0d193f1..b347c1e 100644 --- a/infra/setup/cli.ts +++ b/infra/setup/cli.ts @@ -1,303 +1,15 @@ 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 { runSetupWizard } from "./prompts/wizard.ts"; +import { authCommand } from "./commands/auth.ts"; +import { testCommand } from "./commands/test.ts"; 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; - } - } -} + buildCommand, + compileProtoCommand, + releaseCommand, +} from "./commands/build.ts"; +import { dumpComposeCommand, dumpEnvCommand } from "./commands/compose.ts"; +import { secretsCommand } from "./commands/secrets.ts"; export async function runCli(args = Deno.args): Promise { if (import.meta.main || args.length === 0) { @@ -314,458 +26,18 @@ export async function runCli(args = Deno.args): Promise { const cmd = new Command() .name("auth-setup") .description("Auth setup wizard and CLI") + .version("1.0.0") .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", - ), - ); - }); + .command("auth", authCommand) + .command("test", testCommand) + .command("compile_proto", compileProtoCommand) + .command("build", buildCommand) + .command("release", releaseCommand) + .command("dump_compose", dumpComposeCommand) + .command("dump_env", dumpEnvCommand) + .command("secrets", secretsCommand); await cmd.parse(args); } diff --git a/infra/setup/commands/auth.ts b/infra/setup/commands/auth.ts new file mode 100644 index 0000000..f8f808b --- /dev/null +++ b/infra/setup/commands/auth.ts @@ -0,0 +1,71 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { AuthSetupConfig, DEFAULT_AUTH_CONFIG, readEnv } from "../env.ts"; +import { generateAuthSetupFiles, handleAuthSetup } from "../prompts/auth.ts"; + +export const authCommand = new Command() + .description("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); + } + }); diff --git a/infra/setup/commands/build.ts b/infra/setup/commands/build.ts new file mode 100644 index 0000000..917692a --- /dev/null +++ b/infra/setup/commands/build.ts @@ -0,0 +1,235 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { AuthSetupConfig, DEFAULT_AUTH_CONFIG, readEnv } from "../env.ts"; +import { + downloadWorkloadProto, + executeBuildImage, + executeProtobufCompilation, + generateBuildCommands, +} from "../build.ts"; + +export const compileProtoCommand = new Command() + .description("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); + } + }); + +export const buildCommand = new Command() + .description("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); + } + }); + +export const releaseCommand = new Command() + .description( + "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", + ), + ); + }); diff --git a/infra/setup/commands/compose.ts b/infra/setup/commands/compose.ts new file mode 100644 index 0000000..dd9659d --- /dev/null +++ b/infra/setup/commands/compose.ts @@ -0,0 +1,112 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { + generateDockerCompose, + generateSpireDockerCompose, +} from "../compose.ts"; +import { + AuthSetupConfig, + DEFAULT_AUTH_CONFIG, + generateEnv, + generateSpireEnv, + readEnv, +} from "../env.ts"; + +export const dumpComposeCommand = new Command() + .description("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()); + }); + +export const dumpEnvCommand = new Command() + .description("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)); + }); diff --git a/infra/setup/commands/secrets.ts b/infra/setup/commands/secrets.ts new file mode 100644 index 0000000..036ef6a --- /dev/null +++ b/infra/setup/commands/secrets.ts @@ -0,0 +1,66 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { + AuthSetupConfig, + COMPOSE_PATH, + DEFAULT_AUTH_CONFIG, + ENV_PATH, + readEnv, + SPIRE_COMPOSE_PATH, + SPIRE_ENV_PATH, +} from "../env.ts"; + +export const secretsCommand = new Command() + .description("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", + ), + ); + }); diff --git a/infra/setup/commands/test.ts b/infra/setup/commands/test.ts new file mode 100644 index 0000000..4e76552 --- /dev/null +++ b/infra/setup/commands/test.ts @@ -0,0 +1,61 @@ +import { Command } from "jsr:@cliffy/command@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { AuthSetupConfig, DEFAULT_AUTH_CONFIG, readEnv } from "../env.ts"; + +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.", + ), + ); + } +} + +export const testCommand = new Command() + .description("Test Auth Connection") + .action(async () => { + const loadedEnv = await readEnv(); + const currentConfig: AuthSetupConfig = { + ...DEFAULT_AUTH_CONFIG, + ...loadedEnv, + }; + await handleTestConnection(currentConfig.domainName); + }); diff --git a/infra/setup/prompts/auth.ts b/infra/setup/prompts/auth.ts new file mode 100644 index 0000000..c2aab18 --- /dev/null +++ b/infra/setup/prompts/auth.ts @@ -0,0 +1,134 @@ +import { Input, Secret } 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, + ENV_PATH, + generateEnv, + generateSpireEnv, + SPIRE_COMPOSE_PATH, + SPIRE_ENV_PATH, +} from "../env.ts"; + +export async function promptAuthConfig( + 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 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; + + return { reg, ghcrReg, domainName, appSecret, dbPassword }; +} + +export async function promptSpireConfig( + currentConfig: AuthSetupConfig, +): Promise> { + 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, + }); + + return { dbDataPath, spireDataPath }; +} + +export async function promptConfirmation(): Promise { + // Empty implementation as there was no explicit confirmation in cli.ts before + // But we add it to cleanly delineate the flow as requested. +} + +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 { + const authConfig = await promptAuthConfig(currentConfig); + const spireConfig = await promptSpireConfig(currentConfig); + + const newConfig: AuthSetupConfig = { + ...currentConfig, + ...authConfig, + ...spireConfig, + }; + + await promptConfirmation(); + await generateAuthSetupFiles(newConfig); + + return newConfig; +} diff --git a/infra/setup/prompts/wizard.ts b/infra/setup/prompts/wizard.ts new file mode 100644 index 0000000..37960ae --- /dev/null +++ b/infra/setup/prompts/wizard.ts @@ -0,0 +1,145 @@ +import { Select } from "jsr:@cliffy/prompt@1.0.0-rc.7"; +import * as colors from "jsr:@std/fmt@0.225.2/colors"; +import { + AuthSetupConfig, + COMPOSE_PATH, + DEFAULT_AUTH_CONFIG, + ENV_PATH, + readEnv, + SPIRE_COMPOSE_PATH, +} from "../env.ts"; +import { handleAuthSetup } from "./auth.ts"; +import { handleTestConnection } from "../commands/test.ts"; +import { + downloadWorkloadProto, + executeBuildImage, + executeProtobufCompilation, + generateBuildCommands, +} from "../build.ts"; + +export 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; + } + } +} diff --git a/tasks/new/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.1.md b/tasks/complete/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.1.md similarity index 100% rename from tasks/new/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.1.md rename to tasks/complete/2026-0825.01.jul.story.arch.monolith-decomposition-roadmap-1845.ph4.1.md