Spaces:
Running
Running
File size: 6,435 Bytes
69f9a4b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | import { spawn } from "node:child_process"
import { createServer } from "node:http"
import { fileURLToPath } from "node:url"
import path from "node:path"
import { FIXTURES, freePort, isolatedEnv } from "./predeploy-smoke"
const dockerEnv = () => {
const env = isolatedEnv()
for (const key of ["DOCKER_HOST", "DOCKER_CONTEXT", "DOCKER_TLS", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"]) delete env[key]
return env
}
async function command(args: string[], allowFailure = false) {
const child = spawn("docker", ["--host=unix:///var/run/docker.sock", ...args], { env: dockerEnv(), stdio: ["ignore", "pipe", "pipe"] })
let output = ""
child.stdout.on("data", data => { output += data }); child.stderr.on("data", data => { output += data })
const code = await new Promise<number>((resolve, reject) => { child.once("error", reject); child.once("exit", code => resolve(code ?? 1)) })
if (code && !allowFailure) {
const safe = Object.values(FIXTURES).reduce((text, secret) => text.split(secret).join("[FIXTURE_REDACTED]"), output)
throw new Error(`Docker ${args[0]} failed:\n${safe.slice(-6000)}`)
}
return { code, output }
}
export async function dockerGate() {
try {
if ((await command(["info"], true)).code !== 0) { console.log("SKIPPED_ENV_DOCKER: managed local daemon unavailable"); return { result: "SKIPPED_ENV_DOCKER", passed: 0, failed: 0, skipped: 3 } }
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
console.log("SKIPPED_ENV_DOCKER: docker executable absent"); return { result: "SKIPPED_ENV_DOCKER", passed: 0, failed: 0, skipped: 3 }
}
const args = ["build", "-t", "nodepad-predeploy"]
// Optional CA bundle is only a BuildKit secret; never copied into the image.
if (process.env.CODEX_PROXY_CERT) args.push("--secret", "id=proxy_ca,src=/etc/ssl/certs/ca-certificates.crt")
args.push(".")
await command(args)
const server = createServer(async (req, res) => {
if (req.headers.authorization !== `Bearer ${FIXTURES.OPENAI_API}`) { res.writeHead(401).end(); return }
res.setHeader("Content-Type", "application/json")
if (req.url === "/v1/models") res.end(JSON.stringify({ data: [{ id: "qwythos-9b" }] }))
else if (req.url === "/v1/chat/completions") res.end(JSON.stringify({ choices: [{ message: { content: "NODEPAD_LLM_OK" } }] }))
else res.writeHead(404).end()
})
const providerPort = await new Promise<number>((resolve, reject) => { server.once("error", reject); server.listen(0, "0.0.0.0", () => resolve((server.address() as { port: number }).port)) })
const port = await freePort()
const name = `nodepad-predeploy-${process.pid}`
let passed = 1
const check = (condition: unknown, message: string) => { if (!condition) throw new Error(message); passed++ }
const env = { ...FIXTURES, NODEPAD_GITHUB_SYNC: "false", NODEPAD_MAX_RESEARCH_TASKS_PER_HEARTBEAT: "0", NODEPAD_RESEARCH_ENABLED: "false", NODEPAD_DATA_DIR: "/app/data", OPENAI_URL: `http://host.docker.internal:${providerPort}/v1`, OPENAI_MODEL: "qwythos-9b" }
try {
// Loopback-only publication; no host networking. Docker's configured proxies stay intact.
await command(["run", "-d", "--name", name, "--add-host", "host.docker.internal:host-gateway", "-p", `127.0.0.1:${port}:7860`, ...Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]), "nodepad-predeploy"])
const base = `http://127.0.0.1:${port}`
async function request(url: string, options: RequestInit = {}) {
return fetch(base + url, { ...options, redirect: "manual", signal: AbortSignal.timeout(15_000) })
}
let ready = false
for (let n = 0; n < 100; n++) {
try { if ((await request("/health")).ok) { ready = true; break } } catch { /* startup poll */ }
await new Promise(resolve => setTimeout(resolve, 100))
}
check(ready, "Docker readiness failed")
check((await request("/login")).ok, "Docker login page")
check((await request("/api/v1/workspaces")).status === 401, "Docker anonymous protection")
const uid = (await command(["exec", name, "id", "-u"])).output.trim()
check(uid === "1001", "Docker must run as non-root nextjs UID 1001")
const login = await request("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ accessKey: FIXTURES.NODEPAD_LOGIN_KEY }) })
check(login.ok, "Docker writable session store")
const cookie = (login.headers.get("set-cookie") || "").split(";")[0]
const headers = { Authorization: `Bearer ${FIXTURES.NODEPAD_API_KEY}`, "Content-Type": "application/json" }
check((await request("/api/v1/workspaces/docker/evidence", { method: "POST", headers, body: JSON.stringify({ claim: "Docker persistent fixture" }) })).status === 201, "Docker non-root data write")
check((await request("/api/v1/llm/models", { headers: { Cookie: cookie } })).ok, "Docker local mock discovery")
const completion = await request("/api/v1/llm/test", { method: "POST", headers, body: "{}" })
check(completion.ok && (await completion.json()).content === "NODEPAD_LLM_OK", "Docker local mock inference")
await command(["restart", name])
for (let n = 0; n < 100; n++) { try { if ((await request("/health")).ok) break } catch {} await new Promise(resolve => setTimeout(resolve, 100)) }
const evidence = await request("/api/v1/workspaces/docker/evidence", { headers })
check(evidence.ok && (await evidence.json()).length === 1, "Docker data survives restart")
const sessionStatus = await request("/api/auth/status", { headers: { Cookie: cookie } })
check(sessionStatus.ok && (await sessionStatus.json()).authenticated === true, "Docker session survives restart")
const logs = (await command(["logs", name])).output
check(Object.values(FIXTURES).every(secret => !logs.includes(secret)), "Docker logs must not contain fixture credentials")
console.log(`PASS Docker build/runtime/non-root/persistence: ${passed} assertions`)
return { result: "PASS", passed, failed: 0, skipped: 0 }
} finally {
await command(["rm", "-f", name], true)
server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve()))
}
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) dockerGate().catch(error => {
console.error(error instanceof Error ? error.message : "Docker validation failed"); process.exitCode = 1
})
|