/** * Image recipes + compose derivation. */ import { describe, expect, test } from "bun:test"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { COMPOSE_BASE, COMPOSE_OVERRIDE, DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE, DOCKER_STACK, assertNoCredentialsInYaml, buildCaddyfile, buildNginxConf, buildPgDogToml, buildPgDogUsersToml, builtinRecipes, caddy, cockroach, deriveInfrastructure, dragonfly, emitDockerfile, formatStackEnv, nginx, pgdog, postgres, postgresAdvisor, POSTGRES_ADVISOR_DOCKERFILE, POSTGRES_ADVISOR_IMAGE, recipeFor, redis, resolveStack, SOCKET_PROXY_IMAGE, SOCKET_PROXY_SERVICE, timescale, traefik, traefikAppLabels, valkey, writeDerivedFiles, yugabyte, type ImageRecipe, type ServiceSpec, } from "./index.ts"; /** Service keys under a top-level `services:` mapping (2-space indent). */ function serviceNamesFromComposeYaml(yml: string): Set { const names = new Set(); let inServices = false; for (const line of yml.split("\n")) { if (line === "services:") { inServices = true; continue; } if (!inServices) continue; if (/^[A-Za-z]/.test(line)) break; const m = /^ ([A-Za-z0-9_-]+):\s*$/.exec(line); if (m?.[1]) names.add(m[1]); } return names; } const fixedCreds = { "store.sql": { user: "oke", password: "s3cret-sql-password-xyz", database: "oke", }, "store.kv": { user: "oke", password: "s3cret-kv-password-xyz", database: "oke", }, } as const; describe("image recipes", () => { test("postgres and redis match vendor images by protocol", () => { expect(recipeFor("pgvector/pgvector:pg17").id).toBe("postgres"); expect(postgres.match("postgres:16")).toBe(true); expect(redis.match("redis:7")).toBe(true); expect(redis.match("redis:8-alpine")).toBe(true); expect(redis.match("valkey/valkey:8-alpine")).toBe(false); }); test("valkey matches the official image, keeps redis:// URL, uses valkey-server", () => { const image = "valkey/valkey:8-alpine"; expect(recipeFor(image).id).toBe("valkey"); expect(valkey.match(image)).toBe(true); expect(redis.match(image)).toBe(false); const spec: ServiceSpec = { role: "store.kv", serviceName: "store-kv", image, port: 6379, hostPort: 6379, credentials: fixedCreds["store.kv"], }; const applied = recipeFor(spec.image).apply(spec); expect(String(applied.command)).toContain("exec valkey-server"); expect(String(applied.command)).toContain("$$OKE_STORE_KV_PASSWORD"); expect(applied.healthcheck?.test[1]).toBe("valkey-cli"); const url = recipeFor(spec.image).url(spec, { host: "127.0.0.1", port: 6379, user: "oke", password: fixedCreds["store.kv"].password, database: "oke", }); expect(url).toBe( `redis://:${encodeURIComponent(fixedCreds["store.kv"].password)}@127.0.0.1:6379`, ); const derived = deriveInfrastructure({ images: { "store.kv": image }, credentials: { "store.kv": fixedCreds["store.kv"] }, }); const kvYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(kvYml).toContain(image); expect(kvYml).toContain("valkey-server"); expect(derived.stackEnv.REDIS_URL).toContain("redis://"); }); test("dragonfly matches the official image, keeps redis:// URL, distinct from redis recipe", () => { const image = "docker.dragonflydb.io/dragonflydb/dragonfly"; expect(recipeFor(image).id).toBe("dragonfly"); expect(dragonfly.match(image)).toBe(true); expect(redis.match(image)).toBe(false); const spec: ServiceSpec = { role: "store.kv", serviceName: "store-kv", image, port: 6379, hostPort: 6379, credentials: fixedCreds["store.kv"], }; const applied = recipeFor(spec.image).apply(spec); expect(String(applied.command)).toContain("exec dragonfly"); expect(String(applied.command)).toContain("$$OKE_STORE_KV_PASSWORD"); expect(applied.ulimits?.memlock).toBe(-1); expect(applied.environment?.HEALTHCHECK_PORT).toBe("6379"); expect(applied.healthcheck?.test).toEqual(["CMD", "/usr/local/bin/healthcheck.sh"]); const url = recipeFor(spec.image).url(spec, { host: "127.0.0.1", port: 6379, user: "oke", password: fixedCreds["store.kv"].password, database: "oke", }); expect(url).toBe( `redis://:${encodeURIComponent(fixedCreds["store.kv"].password)}@127.0.0.1:6379`, ); const derived = deriveInfrastructure({ images: { "store.kv": image }, credentials: { "store.kv": fixedCreds["store.kv"] }, }); const kvYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(kvYml).toContain(image); expect(kvYml).toContain("memlock"); expect(kvYml).toContain("HEALTHCHECK_PORT"); expect(derived.stackEnv.REDIS_URL).toContain("redis://"); }); test("cockroach matches official image, port 26257, COCKROACH_* env, sslmode=require URL", () => { const image = "cockroachdb/cockroach:v25.2.0"; expect(recipeFor(image).id).toBe("cockroach"); expect(cockroach.match(image)).toBe(true); expect(postgres.match(image)).toBe(false); const spec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image, port: 26257, hostPort: 5432, credentials: fixedCreds["store.sql"], }; const applied = recipeFor(spec.image).apply(spec); expect(applied.command).toEqual(["start-single-node", "--accept-sql-without-tls"]); expect(applied.environment?.COCKROACH_USER).toBe("${OKE_STORE_SQL_USER}"); expect(applied.environment?.COCKROACH_PASSWORD).toBe("${OKE_STORE_SQL_PASSWORD}"); expect(applied.environment?.COCKROACH_DATABASE).toBe("${OKE_STORE_SQL_DB}"); expect(applied.extraPorts).toEqual([{ host: 8080, container: 8080 }]); expect(applied.volumes?.[0]).toContain("/cockroach/cockroach-data"); const url = recipeFor(spec.image).url(spec, { host: "127.0.0.1", port: 5432, ...fixedCreds["store.sql"], }); expect(url).toContain("sslmode=require"); expect(url).toContain(":5432/"); const derived = deriveInfrastructure({ images: { "store.sql": image }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); expect(derived.specs[0]!.port).toBe(26257); expect(derived.specs[0]!.hostPort).toBe(5432); const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(sqlYml).toContain("26257"); expect(sqlYml).toContain("COCKROACH_"); expect(derived.stackEnv.DATABASE_URL).toContain("sslmode=require"); }); test("yugabyte matches official image, YSQL on 5433, YSQL_* env", () => { const image = "yugabytedb/yugabyte:2025.1.0.0-b100"; expect(recipeFor(image).id).toBe("yugabyte"); expect(yugabyte.match(image)).toBe(true); const spec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image, port: 5433, hostPort: 5432, credentials: fixedCreds["store.sql"], }; const applied = recipeFor(spec.image).apply(spec); expect(String(applied.command)).toContain("yugabyted"); expect(applied.environment?.YSQL_USER).toBe("${OKE_STORE_SQL_USER}"); expect(applied.environment?.YSQL_PASSWORD).toBe("${OKE_STORE_SQL_PASSWORD}"); expect(applied.environment?.YSQL_DB).toBe("${OKE_STORE_SQL_DB}"); expect(applied.volumes?.[0]).toContain("/home/yugabyte/yb_data"); const url = recipeFor(spec.image).url(spec, { host: "127.0.0.1", port: 5432, ...fixedCreds["store.sql"], }); expect(url).toStartWith("postgres://oke:"); const derived = deriveInfrastructure({ images: { "store.sql": image }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); expect(derived.specs[0]!.port).toBe(5433); const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(sqlYml).toContain("5433"); expect(sqlYml).toContain("YSQL_"); }); test("timescale matches ahead of generic postgres with the same POSTGRES_* contract", () => { const image = "timescale/timescaledb:latest-pg17"; expect(recipeFor(image).id).toBe("timescale"); expect(timescale.match(image)).toBe(true); expect(postgres.match(image)).toBe(false); const derived = deriveInfrastructure({ images: { "store.sql": image }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(sqlYml).toContain("POSTGRES_USER"); expect(derived.stackEnv.DATABASE_URL).toContain("postgres://"); }); test("postgres preloads pg_stat_statements at postmaster start", () => { const spec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image: "postgres:18-alpine", port: 5432, hostPort: 5432, credentials: fixedCreds["store.sql"], }; const applied = postgres.apply(spec); expect(applied.command).toEqual([ "postgres", "-c", "shared_preload_libraries=pg_stat_statements", ]); const derived = deriveInfrastructure({ images: { "store.sql": spec.image }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); const yml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(yml).toContain("shared_preload_libraries=pg_stat_statements"); }); test("timescale preload keeps timescaledb first so hypertables still load", () => { const spec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image: "timescale/timescaledb:latest-pg17", port: 5432, hostPort: 5432, credentials: fixedCreds["store.sql"], }; const applied = timescale.apply(spec); expect(applied.command).toEqual([ "postgres", "-c", "shared_preload_libraries=timescaledb,pg_stat_statements", ]); const joined = Array.isArray(applied.command) ? applied.command.join(" ") : String(applied.command); expect(joined.indexOf("timescaledb")).toBeGreaterThanOrEqual(0); expect(joined.indexOf("timescaledb")).toBeLessThan(joined.indexOf("pg_stat_statements")); const derived = deriveInfrastructure({ images: { "store.sql": spec.image }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); const yml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(yml).toContain("timescaledb,pg_stat_statements"); expect(yml).not.toMatch(/shared_preload_libraries=pg_stat_statements[^-]/); }); test("supabase and yugabyte recipes do not overwrite vendor preload commands", () => { const supabaseSpec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image: "supabase/postgres:15.8.1.049", port: 5432, hostPort: 5432, credentials: fixedCreds["store.sql"], }; expect(recipeFor(supabaseSpec.image).apply(supabaseSpec).command).toBeUndefined(); const ybSpec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image: "yugabytedb/yugabyte:2024.2.2.1-b6", port: 5433, hostPort: 5433, credentials: fixedCreds["store.sql"], }; expect(recipeFor(ybSpec.image).apply(ybSpec).command).toEqual([ "bin/yugabyted", "start", "--base_dir=/home/yugabyte/yb_data", "--background=false", ]); }); test("oke-postgres-advisor matches ahead of generic postgres and emits a local build", () => { expect(postgres.match(POSTGRES_ADVISOR_IMAGE)).toBe(true); expect(recipeFor(POSTGRES_ADVISOR_IMAGE).id).toBe("postgres-advisor"); expect(postgresAdvisor.match("postgres:18-alpine")).toBe(false); const derived = deriveInfrastructure({ images: { "store.sql": POSTGRES_ADVISOR_IMAGE }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); const yml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? ""; expect(yml).toContain('dockerfile: "Dockerfile.postgres-advisor"'); expect(yml).toContain("shared_preload_libraries=pg_stat_statements"); const dockerfile = derived.files.find((f) => f.path === POSTGRES_ADVISOR_DOCKERFILE); expect(dockerfile?.content).toContain("FROM postgres:18-alpine"); expect(dockerfile?.content).toContain("HypoPG/hypopg"); expect(dockerfile?.content).toContain("supabase/index_advisor"); expect(dockerfile?.content).toContain("$DOCKER_PG_LLVM_DEPS"); expect(dockerfile?.content).not.toContain("postgresql-dev"); const alpine = deriveInfrastructure({ images: { "store.sql": "postgres:18-alpine" }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); expect(alpine.files.some((f) => f.path === POSTGRES_ADVISOR_DOCKERFILE)).toBe(false); expect(alpine.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "").not.toContain( "Dockerfile.postgres-advisor", ); }); test("pgdog matches the official image and waits on store-sql", () => { expect(recipeFor("ghcr.io/pgdogdev/pgdog:v0.1.59").id).toBe("pgdog"); expect(pgdog.match("ghcr.io/pgdogdev/pgdog:main")).toBe(true); const applied = pgdog.apply({ role: "pgdog", serviceName: "pgdog", image: "ghcr.io/pgdogdev/pgdog:v0.1.59", port: 6432, hostPort: 6432, credentials: fixedCreds["store.sql"], }); expect(applied.dependsOn?.["store-sql"]?.condition).toBe("service_healthy"); expect(applied.volumes).toContain("./pgdog/pgdog.toml:/pgdog/pgdog.toml:ro"); expect(applied.volumes).toContain("./pgdog/users.toml:/pgdog/users.toml:ro"); expect(applied.healthcheck?.test.join(" ")).toContain("pg_isready"); expect(applied.healthcheck?.interval).toBe("2s"); expect(applied.healthcheck?.start_period).toBe("2s"); }); test("pgdog.toml + users.toml match upstream config shape", () => { const pgdogToml = buildPgDogToml({ database: "oke", postgresHost: "store-sql" }); expect(pgdogToml).toContain("[general]"); expect(pgdogToml).toContain('host = "0.0.0.0"'); expect(pgdogToml).toContain("port = 6432"); expect(pgdogToml).toContain('pooler_mode = "transaction"'); expect(pgdogToml).toContain("default_pool_size = 20"); expect(pgdogToml).toContain("[[databases]]"); expect(pgdogToml).toContain('name = "oke"'); expect(pgdogToml).toContain('host = "store-sql"'); expect(pgdogToml).toContain("port = 5432"); expect(pgdogToml).toContain('database_name = "oke"'); expect(pgdogToml).toContain("pool_size = 20"); const usersToml = buildPgDogUsersToml({ user: "oke", password: "s3cret-sql-password-xyz", database: "oke", }); expect(usersToml).toContain("[[users]]"); expect(usersToml).toContain('name = "oke"'); expect(usersToml).toContain('password = "s3cret-sql-password-xyz"'); expect(usersToml).toContain('database = "oke"'); }); test("meilisearch matches the official image and emits a http URL", () => { expect(recipeFor("getmeili/meilisearch:v1.54").id).toBe("meilisearch"); const spec: ServiceSpec = { role: "store.index", serviceName: "store-index", image: "getmeili/meilisearch:v1.54", port: 7700, hostPort: 7700, credentials: { user: "oke", password: "meili-master-key", database: "oke" }, }; const applied = recipeFor(spec.image).apply(spec); expect(applied.environment?.MEILI_MASTER_KEY).toBe("${OKE_STORE_INDEX_KEY}"); expect(applied.volumes).toContain("store-index-data:/meili_data"); const url = recipeFor(spec.image).url(spec, { host: "127.0.0.1", port: 7700, user: "oke", password: "meili-master-key", database: "oke", }); expect(url).toBe("http://127.0.0.1:7700"); }); test("a new image recipe is ≤15 lines", async () => { const src = await Bun.file(`${import.meta.dir}/recipes/postgres.ts`).text(); const exportLines = src .slice(src.indexOf("export const")) .split("\n") .filter((l) => l.trim().length > 0); expect(exportLines.length).toBeLessThanOrEqual(15); }); test("caddy matches official images and emits a Caddyfile reverse_proxy", () => { expect(recipeFor("caddy:2-alpine").id).toBe("caddy"); expect(caddy.match("library/caddy:2")).toBe(true); const spec: ServiceSpec = { role: "proxy", serviceName: "proxy", image: "caddy:2-alpine", port: 80, hostPort: 80, credentials: { user: "oke", password: "unused-proxy", database: "oke" }, }; const applied = caddy.apply(spec); expect(applied.extraPorts).toEqual([{ host: 443, container: 443 }]); expect(applied.volumes).toContain("./Caddyfile:/etc/caddy/Caddyfile:ro"); expect(applied.volumes).toContain("proxy-data:/data"); const file = buildCaddyfile(); expect(file).toContain("{$OKE_PROXY_HOST:localhost}"); expect(file).toContain("reverse_proxy app:6530"); expect(caddy.url(spec, { host: "app.example.com", port: 443, ...spec.credentials })).toBe( "https://app.example.com", ); }); test("nginx matches official images and emits nginx.conf reverse_proxy", () => { expect(recipeFor("nginx:1.31-alpine").id).toBe("nginx"); expect(nginx.match("library/nginx:1.27")).toBe(true); const spec: ServiceSpec = { role: "proxy", serviceName: "proxy", image: "nginx:1.31-alpine", port: 80, hostPort: 80, credentials: { user: "oke", password: "unused-proxy", database: "oke" }, }; const applied = nginx.apply(spec); expect(applied.extraPorts).toBeUndefined(); expect(applied.volumes).toContain("./nginx.conf:/etc/nginx/nginx.conf:ro"); const file = buildNginxConf(); expect(file).toContain("server app:6530"); expect(file).toContain("proxy_pass http://oke_app"); expect(nginx.url(spec, { host: "app.example.com", port: 80, ...spec.credentials })).toBe( "http://app.example.com", ); }); test("traefik matches official images, labels app, and uses socket-proxy", () => { expect(recipeFor("traefik:v3.7").id).toBe("traefik"); expect(traefik.match("traefik:v3.1")).toBe(true); const applied = traefik.apply({ role: "proxy", serviceName: "proxy", image: "traefik:v3.7", port: 80, hostPort: 80, credentials: { user: "oke", password: "unused-proxy", database: "oke" }, }); expect(applied.extraPorts).toEqual([{ host: 443, container: 443 }]); expect(String(applied.command)).toContain("--providers.docker=true"); expect(String(applied.command)).toContain(`tcp://${SOCKET_PROXY_SERVICE}:2375`); expect(String(applied.command)).not.toContain("docker.sock"); expect(applied.volumes?.some((v) => v.includes("docker.sock"))).toBeFalsy(); expect(applied.dependsOn?.[SOCKET_PROXY_SERVICE]?.condition).toBe("service_started"); const socket = applied.services?.[SOCKET_PROXY_SERVICE] as Record; expect(socket.image).toBe(SOCKET_PROXY_IMAGE); expect(socket.volumes).toContain("/var/run/docker.sock:/var/run/docker.sock:ro"); const labels = (applied.services?.app as { labels: Record }).labels; expect(labels).toEqual(traefikAppLabels()); expect(labels["traefik.enable"]).toBe("true"); expect(labels["traefik.http.services.app.loadbalancer.server.port"]).toBe("6530"); expect(labels["traefik.http.routers.app.rule"]).toContain("OKE_PROXY_HOST"); }); test("proxy recipes do not filter HTTP methods", () => { expect(buildCaddyfile()).toContain("reverse_proxy app:6530"); expect(buildCaddyfile()).not.toMatch(/method\s|allowed_methods|@method/i); expect(buildNginxConf()).not.toMatch(/limit_except/i); const labels = traefikAppLabels(); expect(JSON.stringify(labels)).not.toMatch(/Method\(|methods/i); }); test("recipe.url builds a connection string without env-var names in the kernel", () => { const spec: ServiceSpec = { role: "store.sql", serviceName: "store-sql", image: "postgres:16", port: 5432, hostPort: 5432, credentials: fixedCreds["store.sql"], }; const url = postgres.url(spec, { host: "127.0.0.1", port: 5432, ...fixedCreds["store.sql"], }); expect(url).toStartWith("postgres://oke:"); expect(url).toContain("@127.0.0.1:5432/oke"); expect(url).not.toContain("POSTGRES_"); }); test("plugin recipe can be registered in ≤15 lines", () => { const nats: ImageRecipe = { id: "nats", port: 4222, match: (i) => /nats/i.test(i), apply: () => ({ command: ["--js"], healthcheck: { test: ["CMD", "nats-server", "--signal", "ldm"], interval: "5s", timeout: "3s", retries: 5, }, }), url: (_s, c) => `nats://${c.host}:${c.port}`, }; const src = `export const nats: ImageRecipe = { id: "nats", port: 4222, match: (i) => /nats/i.test(i), apply: () => ({ command: ["--js"], healthcheck: { test: ["CMD", "nats-server", "--signal", "ldm"], interval: "5s", timeout: "3s", retries: 5 } }), url: (_s, c) => \`nats://\${c.host}:\${c.port}\`, };`; expect(src.trim().split("\n").length).toBeLessThanOrEqual(15); expect(recipeFor("nats:2.10", [nats]).id).toBe("nats"); expect(builtinRecipes.length).toBeGreaterThanOrEqual(2); }); }); describe("deriveInfrastructure", () => { test("emits Dockerfile + single docker-compose.yml by default", () => { const result = deriveInfrastructure({ images: { "store.sql": "pgvector/pgvector:pg17", "store.kv": "valkey/valkey:8-alpine", }, credentials: fixedCreds, app: "skyport", }); const paths = result.files.map((f) => f.path); expect(paths).toContain("Dockerfile"); expect(paths).toContain(DOCKER_COMPOSE); expect(paths).not.toContain(COMPOSE_BASE); expect(paths).not.toContain("compose.store.sql.yml"); expect(paths).not.toContain(COMPOSE_OVERRIDE); expect(paths).not.toContain(DOCKER_COMPOSE_OVERRIDE); expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).toContain("pgvector/pgvector:pg17"); expect(yml).toContain("valkey/valkey:8-alpine"); expect(yml).toContain("POSTGRES_PASSWORD"); expect(yml).toContain("${OKE_STORE_SQL_PASSWORD}"); expect(yml).toContain(".env.local"); expect(yml).not.toContain(fixedCreds["store.sql"].password); expect(yml).toContain('context: ".."'); expect(yml).toContain("app:"); expect(yml).toContain("oke-skyport:latest"); for (const f of result.files) { assertNoCredentialsInYaml(f.content, Object.values(fixedCreds)); } expect(result.stackEnv.DATABASE_URL).toContain("postgres://"); expect(result.stackEnv.OKE_STORE_SQL_PASSWORD).toBe(fixedCreds["store.sql"].password); }); test("compose YAML spaces services and adds role comments", () => { const result = deriveInfrastructure({ images: { "channel.email": "axllent/mailpit:v1.31.2", pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59", "store.sql": "postgres:16", }, credentials: { "store.sql": fixedCreds["store.sql"] }, includeApp: false, app: "dev", }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).toContain("# Generated by `oke docker`"); expect(yml).toContain("# channel.email — Mailpit"); expect(yml).toContain("# pgdog — connection pooler"); expect(yml).toContain("# store.sql — Postgres"); // Blank line between adjacent service blocks (comment → key → … → blank → comment). expect(yml).toMatch(/# channel\.email —[^\n]+\n channel-email:\n[\s\S]+\n\n # pgdog —/); expect(yml).toMatch(/\nnetworks:\n[\s\S]+\n\nservices:\n/); expect(serviceNamesFromComposeYaml(yml)).toEqual( new Set(["channel-email", "pgdog", "store-sql"]), ); }); test("includeApp false omits app service (infra-only stack)", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, includeApp: false, app: "dev", }); const base = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(base).toContain("oke-dev"); expect(base).toContain("networks:"); expect(base).not.toContain("app:"); expect(base).not.toContain("build:"); }); test("includeApp false + Traefik omits the label-only app overlay", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16", proxy: "traefik:v3.7", }, credentials: { "store.sql": fixedCreds["store.sql"] }, includeApp: false, app: "dev", }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; const services = serviceNamesFromComposeYaml(yml); expect(services.has("app")).toBe(false); expect(services.has("proxy")).toBe(true); expect(services.has(SOCKET_PROXY_SERVICE)).toBe(true); expect(yml).toContain("traefik:v3.7"); expect(yml).not.toContain("build:"); }); test("prod folds readiness + budgeted deploy into docker-compose.yml", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, prod: true, serverCpus: 4, serverMemoryGb: 8, }); expect(result.files.some((f) => f.path === "compose.prod.yml")).toBe(false); expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).toContain("replicas"); expect(yml).toContain("/_/ready"); expect(yml).toContain("update_config"); expect(yml).toContain("restart_policy"); expect(yml).toContain("stop_grace_period"); expect(yml).toContain("start-first"); expect(yml).toContain("on-failure"); expect(yml).toContain("cpus:"); expect(yml).toContain("memory:"); }); test("layout split emits per-role files + compose.prod.yml", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, prod: true, layout: "split", }); expect(result.files.some((f) => f.path === COMPOSE_BASE)).toBe(true); expect(result.files.some((f) => f.path === "compose.store.sql.yml")).toBe(true); expect(result.files.some((f) => f.path === "compose.prod.yml")).toBe(true); expect(result.files.some((f) => f.path === DOCKER_COMPOSE)).toBe(false); expect(result.composeFiles).toEqual([ COMPOSE_BASE, "compose.store.sql.yml", "compose.prod.yml", COMPOSE_OVERRIDE, ]); const prod = result.files.find((f) => f.path === "compose.prod.yml")!.content; expect(prod).toContain("replicas"); expect(prod).toContain("/_/ready"); expect(prod).toContain("cpus:"); }); test("layout stack emits docker-stack.yml with overlay network", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, prod: true, layout: "stack", app: "skyport", }); expect(result.files.map((f) => f.path)).toContain(DOCKER_STACK); expect(result.files.some((f) => f.path === DOCKER_COMPOSE)).toBe(false); expect(result.composeFiles).toEqual([DOCKER_STACK]); const yml = result.files.find((f) => f.path === DOCKER_STACK)!.content; expect(yml).toContain("overlay"); expect(yml).toContain("oke-skyport:latest"); expect(yml).not.toContain("build:"); expect(yml).toContain("/_/ready"); expect(yml).toContain("cpus:"); }); test("when postgres + pgdog are both pinned, DATABASE_URL points at PgDog", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:18-alpine", pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59", }, credentials: { "store.sql": fixedCreds["store.sql"] }, prod: true, app: "skyport", }); expect(result.stackEnv.DATABASE_URL).toContain(":6432/"); expect(result.stackEnv.DATABASE_URL).toContain("postgres://oke:"); expect(result.stackEnv.DATABASE_URL).toContain("/oke"); expect(result.stackEnv.OKE_PGDOG_URL).toBe(result.stackEnv.DATABASE_URL); // Direct Postgres URL stays on the role key for ops / escape hatch. expect(result.stackEnv.OKE_STORE_SQL_URL).toContain(":5432/"); const pgdogYml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(pgdogYml).toContain("ghcr.io/pgdogdev/pgdog:v0.1.59"); expect(pgdogYml).toContain("store-sql"); expect(pgdogYml).toContain("service_healthy"); expect(pgdogYml).not.toContain(fixedCreds["store.sql"].password); const pgdogToml = result.files.find((f) => f.path === "pgdog/pgdog.toml")!.content; expect(pgdogToml).toContain('pooler_mode = "transaction"'); expect(pgdogToml).toContain("pool_size = 20"); expect(pgdogToml).toContain('host = "store-sql"'); expect(pgdogToml).not.toContain(fixedCreds["store.sql"].password); const usersToml = result.files.find((f) => f.path === "pgdog/users.toml")!.content; expect(usersToml).toContain('password = "s3cret-sql-password-xyz"'); expect(usersToml).toContain('database = "oke"'); }); test("postgres alone keeps DATABASE_URL on 5432 (no pooler)", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:18-alpine" }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); expect(result.stackEnv.DATABASE_URL).toContain(":5432/"); expect(result.stackEnv.OKE_PGDOG_URL).toBeUndefined(); expect(result.files.some((f) => f.path === "pgdog/pgdog.toml")).toBe(false); }); test("Dockerfile CMD is oke start", () => { const df = emitDockerfile(); expect(df).toContain('CMD ["oke", "start"]'); expect(df).toContain("oven/bun:1.4"); }); test("writeDerivedFiles never writes compose.override.yml", async () => { const root = await mkdtemp(join(tmpdir(), "oke-docker-")); const dockerDir = join(root, "docker"); const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); const written = await writeDerivedFiles(result, dockerDir, { writeStackEnv: true, }); expect(written.some((p) => p.endsWith(COMPOSE_OVERRIDE))).toBe(false); expect(await Bun.file(join(root, ".env.local")).exists()).toBe(true); expect(await Bun.file(join(dockerDir, DOCKER_COMPOSE)).exists()).toBe(true); const envText = await Bun.file(join(root, ".env.local")).text(); expect(envText).toContain("DATABASE_URL="); expect(envText).toContain("# .env.local — stack keys written by"); expect(envText).toContain("# ── store.sql — Postgres"); expect(formatStackEnv(result.stackEnv)).toContain("OKE_STORE_SQL_PASSWORD="); }); test("writeDerivedFiles merges stack keys into existing .env.local", async () => { const root = await mkdtemp(join(tmpdir(), "oke-docker-")); const dockerDir = join(root, "docker"); await Bun.write(join(root, ".env.local"), "GITHUB_TOKEN=keep-me\n"); const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); await writeDerivedFiles(result, dockerDir, { writeStackEnv: true }); const envText = await Bun.file(join(root, ".env.local")).text(); expect(envText).toContain("GITHUB_TOKEN=keep-me"); expect(envText).toContain("DATABASE_URL="); }); test("store.index meilisearch emits its own URL + master key env", () => { const result = deriveInfrastructure({ images: { "store.index": "getmeili/meilisearch:v1.54" }, credentials: { "store.index": { user: "oke", password: "meili-master-key", database: "oke" }, }, app: "skyport", }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).toContain("getmeili/meilisearch:v1.54"); expect(yml).toContain("${OKE_STORE_INDEX_KEY}"); expect(yml).toContain("meili_data"); expect(yml).not.toContain("meili-master-key"); expect(result.stackEnv.OKE_STORE_INDEX_URL).toBe("http://127.0.0.1:7700"); expect(result.stackEnv.OKE_STORE_INDEX_KEY).toBe("meili-master-key"); expect(result.stackEnv.DATABASE_URL).toBeUndefined(); }); test("emits protocol-specific env keys plus optional control notes", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:18-alpine", "store.kv": "redis:8-alpine", "store.files": "rustfs/rustfs:1.0.0", "channel.email": "axllent/mailpit:v1.31.2", }, credentials: { ...fixedCreds, "store.files": { user: "files-key", password: "files-secret", database: "oke", }, "channel.email": { user: "unused", password: "unused-secret", database: "oke", }, }, }); expect(result.stackEnv.S3_ACCESS_KEY_ID).toBe("files-key"); expect(result.stackEnv.S3_SECRET_ACCESS_KEY).toBe("files-secret"); expect(result.stackEnv.S3_BUCKET).toBe("oke"); expect(result.stackEnv.S3_URL).toContain("http://files-key:"); expect(result.stackEnv.S3_ENDPOINT).toBe("http://127.0.0.1:9000"); expect(result.stackEnv.S3_CONSOLE_URL).toBe("http://127.0.0.1:9001"); expect(result.stackEnv.SMTP_URL).toBe("smtp://127.0.0.1:1025"); expect(result.stackEnv.SMTP_HOST).toBe("127.0.0.1"); expect(result.stackEnv.SMTP_PORT).toBe("1025"); expect(result.stackEnv.MAILPIT_UI_URL).toBe("http://127.0.0.1:8025"); expect(result.stackEnv.OKE_STORE_KV_PASSWORD).toBe(fixedCreds["store.kv"].password); expect(result.stackEnv.OKE_STORE_KV_USER).toBeUndefined(); expect(result.stackEnv.OKE_STORE_KV_DB).toBeUndefined(); expect(result.stackEnv.OKE_CHANNEL_EMAIL_PASSWORD).toBeUndefined(); const text = formatStackEnv(result.stackEnv); expect(text).toContain("# OKE_STORE_KV_MAXMEMORY=256mb"); expect(text).toContain("# S3_SESSION_TOKEN="); expect(text).toContain("# SMTP_USER="); expect(text).toContain("# POSTGRES_INITDB_ARGS=--data-checksums"); }); test("instance-offset UI aliases land in stack env", () => { const id = "a3f791"; const n = Number.parseInt(id.slice(0, 4), 16) % 1000; const result = deriveInfrastructure({ images: { "channel.email": "axllent/mailpit:v1.31.2", "store.files": "rustfs/rustfs:1.0.0", }, instanceId: id, credentials: { "channel.email": { user: "oke", password: "unused-mail-password", database: "oke", }, "store.files": { user: "files-key", password: "files-secret", database: "oke", }, }, }); expect(result.stackEnv.SMTP_PORT).toBe(String(20_000 + n)); expect(result.stackEnv.MAILPIT_UI_URL).toBe(`http://127.0.0.1:${21_000 + n}`); expect(result.stackEnv.S3_ENDPOINT).toBe(`http://127.0.0.1:${18_000 + n}`); expect(result.stackEnv.S3_CONSOLE_URL).toBe(`http://127.0.0.1:${19_000 + n}`); }); test("resolveStack previews without writing", () => { const rows = resolveStack({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, }); expect(rows).toHaveLength(1); expect(rows[0]!.recipe).toBe("postgres"); expect(rows[0]!.hostPort).toBe(5432); expect(rows[0]!.extraPorts).toEqual([]); }); test("resolveStack includes instance-offset Mailpit UI / RustFS console", () => { const id = "a3f791"; const n = Number.parseInt(id.slice(0, 4), 16) % 1000; const rows = resolveStack({ images: { "channel.email": "axllent/mailpit:v1.31.2", "store.files": "rustfs/rustfs:1.0.0", }, instanceId: id, credentials: { "channel.email": { user: "oke", password: "stack-preview-mail-password", database: "oke", }, "store.files": { user: "oke", password: "stack-preview-files-password", database: "oke", }, }, }); const mail = rows.find((r) => r.role === "channel.email")!; const files = rows.find((r) => r.role === "store.files")!; expect(mail.hostPort).toBe(20_000 + n); expect(mail.extraPorts).toEqual([{ hostPort: 21_000 + n, containerPort: 8025 }]); expect(files.hostPort).toBe(18_000 + n); expect(files.extraPorts).toEqual([{ hostPort: 19_000 + n, containerPort: 9001 }]); }); test("opt-in caddy proxy emits Caddyfile and unpublishes app ports", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16", proxy: "caddy:2-alpine", }, credentials: { "store.sql": fixedCreds["store.sql"] }, app: "skyport", }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).toContain("app:"); expect(yml).not.toContain("6530:6530"); expect(yml).toContain("proxy:"); expect(yml).toContain("store-sql"); expect(yml).toContain("caddy:2-alpine"); expect(yml).toContain("80:80"); expect(yml).toContain("443:443"); expect(yml).toContain("./Caddyfile:/etc/caddy/Caddyfile:ro"); // App depends_on must not wait on the edge proxy. const appBlock = yml.split("store-sql:")[0]!; expect(appBlock).not.toMatch(/depends_on:[\s\S]*proxy/); const caddyfile = result.files.find((f) => f.path === "Caddyfile")!.content; expect(caddyfile).toContain("reverse_proxy app:6530"); expect(result.stackEnv.OKE_PROXY_URL).toBe("https://127.0.0.1"); expect(formatStackEnv(result.stackEnv)).toContain("# ── proxy — edge reverse proxy"); expect(formatStackEnv(result.stackEnv)).toContain("# OKE_PROXY_HOST=localhost"); }); test("opt-in traefik proxy labels app and mounts socket only on socket-proxy", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16", proxy: "traefik:v3.7", }, credentials: { "store.sql": fixedCreds["store.sql"] }, app: "skyport", prod: true, }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).not.toContain("6530:6530"); expect(yml).toContain("traefik:v3.7"); expect(yml).toContain(SOCKET_PROXY_IMAGE); expect(yml).toContain(SOCKET_PROXY_SERVICE); expect(yml).toContain("/var/run/docker.sock:/var/run/docker.sock:ro"); expect(yml).toContain("tcp://socket-proxy:2375"); expect(yml).toContain("traefik.enable"); expect(yml).toContain("traefik.http.routers.app.rule"); expect(yml).toContain("loadbalancer.server.port"); // Raw socket must not be on the Traefik service itself. const traefikBlock = yml.split("socket-proxy:")[0]!; expect(traefikBlock).toContain("traefik:v3.7"); expect(traefikBlock).not.toContain("docker.sock"); expect(result.files.some((f) => f.path === "Caddyfile")).toBe(false); expect(result.stackEnv.OKE_PROXY_URL).toBe("https://127.0.0.1"); expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]); }); test("opt-in nginx proxy emits nginx.conf and unpublishes app ports", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16", proxy: "nginx:1.31-alpine", }, credentials: { "store.sql": fixedCreds["store.sql"] }, app: "skyport", }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(yml).not.toContain("6530:6530"); expect(yml).toContain("nginx:1.31-alpine"); expect(yml).toContain("./nginx.conf:/etc/nginx/nginx.conf:ro"); const conf = result.files.find((f) => f.path === "nginx.conf")!.content; expect(conf).toContain("proxy_pass http://oke_app"); expect(conf).toContain("server app:6530"); expect(result.stackEnv.OKE_PROXY_URL).toBe("http://127.0.0.1"); }); test("without proxy role, app still publishes 6530 (default unchanged)", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16" }, credentials: { "store.sql": fixedCreds["store.sql"] }, app: "skyport", }); const base = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; expect(base).toContain("6530:6530"); expect(base).not.toContain("\n proxy:"); expect(result.stackEnv.OKE_PROXY_URL).toBeUndefined(); }); test("docker-compose.yml includes every role + companion service when prod", () => { const result = deriveInfrastructure({ images: { "store.sql": "postgres:16", "store.kv": "redis:8-alpine", proxy: "traefik:v3.7", }, credentials: { "store.sql": fixedCreds["store.sql"], "store.kv": fixedCreds["store.kv"], }, app: "skyport", prod: true, }); const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content; const services = serviceNamesFromComposeYaml(yml); expect(services.has("app")).toBe(true); expect(services.has("store-sql")).toBe(true); expect(services.has("store-kv")).toBe(true); expect(services.has("proxy")).toBe(true); expect(services.has(SOCKET_PROXY_SERVICE)).toBe(true); expect(yml).toContain("/_/ready"); expect(yml).toContain("stop_grace_period"); expect(yml).toContain("update_config"); expect(yml).toContain("cpus:"); expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]); }); });