/** * Network diagnostic module. * * Uses exec to inspect Docker networks, Traefik proxy, DNS, and container connectivity. * All commands run inside the application container via the Coolify exec API. * * @module */ import { isErr, type Result, ok, err } from "@mks2508/no-throw"; import type { CoolifyService } from "./coolify/index.js"; import { OperationTracer, type IOperationTrace } from "./trace.js"; /** * Container network information. */ export interface INetworkInfo { /** /etc/hosts entries */ hosts: string[]; /** DNS resolution results */ dns: Array<{ hostname: string; resolved: boolean; ip?: string }>; /** Container environment variables related to networking */ networkEnv: Record; /** Reachable services (connectivity test results) */ connectivity: Array<{ target: string; reachable: boolean; responseTime?: number; }>; /** Container's network interfaces */ interfaces: string[]; /** Operation trace */ trace: IOperationTrace; } /** * Deploy failure analysis. */ export interface IDeployFailureAnalysis { /** Deployment UUID */ deploymentUuid: string; /** Deployment status */ status: string; /** Raw build logs */ rawLogs: string; /** Extracted error lines */ errors: string[]; /** Likely error category */ category: | "build_error" | "install_error" | "docker_error" | "network_error" | "timeout" | "unknown"; /** Human-readable summary */ summary: string; /** Suggested fix */ suggestion: string; /** Operation trace */ trace: IOperationTrace; } /** Common error patterns in build logs. */ const ERROR_PATTERNS: Array<{ pattern: RegExp; category: IDeployFailureAnalysis["category"]; summary: string; suggestion: string; }> = [ { pattern: /npm ERR!|yarn error|pnpm ERR/i, category: "install_error", summary: "Package installation failed", suggestion: "Check package.json for invalid dependencies. Try deleting lock file and rebuilding.", }, { pattern: /COPY failed|COPY --from.*not found|no such file/i, category: "docker_error", summary: "Dockerfile COPY failed — file not found", suggestion: "Check Dockerfile paths and base_directory. Ensure source files exist in build context.", }, { pattern: /error TS\d+|TypeError|SyntaxError|ReferenceError/i, category: "build_error", summary: "TypeScript/JavaScript compilation error", suggestion: "Fix the type errors locally before deploying. Run `bun run typecheck`.", }, { pattern: /ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|getaddrinfo/i, category: "network_error", summary: "Network connectivity issue during build", suggestion: "Server may have DNS issues or firewall blocking outbound connections. Check server network.", }, { pattern: /OOMKilled|out of memory|Cannot allocate memory/i, category: "build_error", summary: "Out of memory during build", suggestion: "Server ran out of RAM. Use a build server or increase server memory.", }, { pattern: /context deadline exceeded|timed out/i, category: "timeout", summary: "Operation timed out", suggestion: "Build took too long. Check if server is overloaded or network is slow.", }, { pattern: /permission denied|EACCES/i, category: "docker_error", summary: "Permission denied", suggestion: "Check file permissions in Dockerfile. Ensure non-root user has access to needed paths.", }, { pattern: /exec.*not found|command not found/i, category: "build_error", summary: "Command not found during build", suggestion: "A required binary is missing in the Docker image. Add it to the Dockerfile or use a different base image.", }, ]; /** * Safely execute a command, returning output or empty string on failure. */ async function safeExec( svc: CoolifyService, uuid: string, command: string, ): Promise { const result = await svc.executeCommand(uuid, command); if (isErr(result)) return ""; return result.value.response || result.value.message || ""; } /** * Inspects the network environment of an application container. * * @param svc - CoolifyService instance * @param appUuid - Application UUID * @param servicesToTest - Optional list of service names to test connectivity (e.g., ['db', 'redis']) * @returns Network diagnostic information */ export async function inspectNetwork( svc: CoolifyService, appUuid: string, servicesToTest: string[] = [], ): Promise> { const tracer = new OperationTracer("network:inspect"); try { tracer.step("Fetching /etc/hosts"); const hostsRaw = await safeExec(svc, appUuid, "cat /etc/hosts 2>/dev/null"); const hosts = hostsRaw .split("\n") .filter((l) => l.trim() && !l.startsWith("#")); tracer.step("Checking network interfaces"); const ifRaw = await safeExec( svc, appUuid, "ip addr 2>/dev/null || ifconfig 2>/dev/null || cat /proc/net/if_inet6 2>/dev/null || echo 'no network tools'", ); const interfaces = ifRaw .split("\n") .filter((l) => l.includes("inet") || l.includes("scope")); tracer.step("Extracting network env vars"); const envRaw = await safeExec( svc, appUuid, "env 2>/dev/null | grep -iE '(HOST|PORT|URL|DATABASE|REDIS|MONGO|POSTGRES|MYSQL|DB_|COOLIFY)' | sort", ); const networkEnv: Record = {}; for (const line of envRaw.split("\n").filter(Boolean)) { const [key, ...rest] = line.split("="); if (key) networkEnv[key] = rest.join("="); } tracer.step("Testing DNS resolution"); const dnsTargets = [ ...servicesToTest, ...Object.values(networkEnv) .filter((v) => !v.includes("/") && !v.includes(":")) .filter((v) => /^[a-z][a-z0-9-]*$/i.test(v)), ]; const uniqueTargets = [...new Set(dnsTargets)].slice(0, 10); const dns: INetworkInfo["dns"] = []; for (const hostname of uniqueTargets) { const nslookup = await safeExec( svc, appUuid, `getent hosts ${hostname} 2>/dev/null || nslookup ${hostname} 2>/dev/null || echo 'FAIL'`, ); const resolved = !nslookup.includes("FAIL") && nslookup.trim().length > 0; const ip = resolved ? nslookup.split(/\s+/)[0] : undefined; dns.push({ hostname, resolved, ip }); } tracer.step( "Testing service connectivity", `${uniqueTargets.length} targets`, ); const connectivity: INetworkInfo["connectivity"] = []; for (const target of servicesToTest) { const start = Date.now(); const curlResult = await safeExec( svc, appUuid, `timeout 3 sh -c "echo > /dev/tcp/${target}/80 2>/dev/null && echo OK || curl -sf --max-time 2 http://${target}/ >/dev/null 2>&1 && echo OK || echo FAIL"`, ); connectivity.push({ target, reachable: curlResult.includes("OK"), responseTime: Date.now() - start, }); } const trace = tracer.finish(true); return ok({ hosts, dns, networkEnv, connectivity, interfaces, trace }); } catch (error) { const trace = tracer.finish( false, error instanceof Error ? error.message : String(error), ); return err(new Error(`Network inspection failed: ${trace.error}`)); } } /** * Analyzes a failed deployment, extracting errors from build logs. * * @param svc - CoolifyService instance * @param deploymentUuid - Deployment UUID * @returns Failure analysis with categorized errors */ export async function analyzeDeployFailure( svc: CoolifyService, deploymentUuid: string, ): Promise> { const tracer = new OperationTracer("deploy:analyze"); try { tracer.step("Fetching deployment details"); const deployResult = await svc.getDeploymentLogs(deploymentUuid); if (isErr(deployResult)) { return err(deployResult.error); } const { status, logs: rawLogs } = deployResult.value; tracer.step("Extracting error lines", `${rawLogs.length} chars`); const lines = rawLogs.split("\n"); const errors = lines.filter( (l) => /error|ERR!|failed|FAIL|fatal|panic|exception/i.test(l) && !/no error|success/i.test(l), ); tracer.step("Categorizing error"); let matchedPattern = ERROR_PATTERNS.find((p) => p.pattern.test(rawLogs)); if (!matchedPattern) { matchedPattern = { pattern: /./, category: "unknown", summary: "Unknown build failure", suggestion: "Check the full build logs with `build-logs ` for details.", }; } const trace = tracer.finish(true); return ok({ deploymentUuid, status, rawLogs, errors: errors.slice(0, 20), category: matchedPattern.category, summary: matchedPattern.summary, suggestion: matchedPattern.suggestion, trace, }); } catch (error) { const trace = tracer.finish( false, error instanceof Error ? error.message : String(error), ); return err(new Error(`Deploy analysis failed: ${trace.error}`)); } }