auth-yes/infra/setup.ts

1018 lines
30 KiB
TypeScript

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<Partial<AuthSetupConfig>> {
const config: Partial<AuthSetupConfig> = {};
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"
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<void> {
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<void> {
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<void> {
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<void> {
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<AuthSetupConfig> {
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<void> {
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<void> {
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<void> {
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;
}
}
}
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 <reg:string>", "Container Registry URL")
.option("--ghcr-registry <ghcrReg:string>", "GHCR Mirror Registry URL")
.option("--domain <domain:string>", "Auth Domain Name")
.option("--db-path <path:string>", "Database Path on the Host")
.option("--spire-path <path:string>", "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);
}