diff --git a/src/app/api/deploy/route.ts b/src/app/api/deploy/route.ts index fdddf5f..8639ff5 100644 --- a/src/app/api/deploy/route.ts +++ b/src/app/api/deploy/route.ts @@ -2,11 +2,15 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; import { deployApp } from "@/core/deployment"; -import { Application } from "@/lib/types"; +import { Application, Port } from "@/lib/types"; export async function POST(req: NextRequest): Promise { const auth = await testAuth(req); - const { name, app }: { name: string; app: Application } = await req.json(); + const { + name, + app, + ports, + }: { name: string; app: Application; ports: Port[] } = await req.json(); if (auth) { return NextResponse.json( @@ -22,7 +26,7 @@ export async function POST(req: NextRequest): Promise { ); } - const deployId = await deployApp(name, app); + const deployId = await deployApp(name, app, ports); return NextResponse.json({ message: "ok", diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx index dd6c53d..69b9444 100644 --- a/src/app/logs/page.tsx +++ b/src/app/logs/page.tsx @@ -20,7 +20,7 @@ export default function LogsPage() { const { value: logs, setValue: setLogs } = useAsyncInterval( [], async () => (isPaused ? ([] as Log[]) : getAllContainerLogs()), - 100 + 1000 ); const filteredLogs = logs.filter((log) => diff --git a/src/components/dialogs/DeployApp.tsx b/src/components/dialogs/DeployApp.tsx index 3a6fede..56351ab 100644 --- a/src/components/dialogs/DeployApp.tsx +++ b/src/components/dialogs/DeployApp.tsx @@ -17,8 +17,9 @@ import { getLiveNodes } from "@/lib/dockship/node"; import { Combobox } from "../ui/combobox"; import { deployApp } from "@/lib/dockship/deploy"; import { useAsync } from "@/hooks/use-async"; -import { Application } from "@/lib/types"; +import { Application, Port } from "@/lib/types"; import { getApps } from "@/lib/dockship/application"; +import { Plus } from "lucide-react"; export default function DeployAppDialog({ defaultApp, @@ -33,9 +34,26 @@ export default function DeployAppDialog({ const [app, setApp] = useState(defaultApp || ""); const [node, setNode] = useState(""); + const [ports, setPorts] = useState([]); + const [newPort, setNewPort] = useState({ + containerPort: "", + hostPort: "", + protocol: "tcp", + }); + + const addPort = () => { + if (!newPort.containerPort || !newPort.hostPort) return; + setPorts([...ports, newPort]); + setNewPort({ containerPort: "", hostPort: "", protocol: "tcp" }); + }; + + const removePort = (index: number) => { + setPorts(ports.filter((_, i) => i !== index)); + }; + const deploy = async () => { - console.log(name, app, node); - await deployApp(name, app, node); + console.log("Deploying:", { name, app, node, ports }); + await deployApp(name, app, node, ports); }; return ( @@ -48,6 +66,7 @@ export default function DeployAppDialog({
+ {/* Name */}
+ {/* Application */}
+ {/* Node */}
+ + {/* Ports */} +
+ +
+ + setNewPort({ ...newPort, containerPort: e.target.value }) + } + /> + + setNewPort({ ...newPort, hostPort: e.target.value }) + } + /> + + setNewPort({ + ...newPort, + protocol: v as "tcp" | "udp", + }) + } + data={[ + { label: "TCP", value: "tcp" }, + { label: "UDP", value: "udp" }, + ]} + placeholder="Select protocol..." + listPlaceholder="No protocols found :(" + /> + +
+ + {ports.length > 0 && ( +
    + {ports.map((p, i) => ( +
  • + + {p.containerPort} → {p.hostPort} ( + {p.protocol.toUpperCase()}) + + +
  • + ))} +
+ )} +
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 6c6c953..85bbb49 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -43,13 +43,13 @@ export function Combobox({ variant="outline" role="combobox" aria-expanded={open} - className="w-50 justify-between" + className="justify-between" > {value ? data.find((itm) => itm.value === value)?.label : placeholder} - + diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 3fe7c25..99f6e7b 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -1,7 +1,7 @@ "use server"; import Docker from "dockerode"; -import { Application, Deployment } from "@/lib/types"; +import { Application, Deployment, Port } from "@/lib/types"; import { CPUusage } from "@/lib/server/calc"; import path from "path"; import fs from "fs"; @@ -20,6 +20,20 @@ function sanitizeString(str: string): string { ); } +function formatPorts(ports: Port[]) { + const dockerPorts: Record = {}; + + ports.forEach(({ containerPort, hostPort, protocol }) => { + const key = `${containerPort}/${protocol}`; + if (!dockerPorts[key]) { + dockerPorts[key] = []; + } + dockerPorts[key].push({ HostPort: hostPort }); + }); + + return dockerPorts; +} + export async function getDeployments( apps: Application[] ): Promise { @@ -96,7 +110,8 @@ export async function getDeployments( export async function deployApp( name: string, - app: Application + app: Application, + ports: Port[] ): Promise { const deployId = randomUUID(); const deployPath = path.join("data", "deploys", deployId); @@ -134,9 +149,7 @@ export async function deployApp( Tty: true, HostConfig: { RestartPolicy: { Name: "unless-stopped" }, - PortBindings: { - "3000/tcp": [{ HostPort: "3800" }], - }, + PortBindings: formatPorts(ports), }, Env: Object.entries(app.env).map( ([key, { value }]) => `${key}="${value.replaceAll('"', '\\"')}"` diff --git a/src/core/docker.ts b/src/core/docker.ts index e8e203a..42c0101 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -67,36 +67,52 @@ function demuxLogs(buffer: Buffer): string { return output; } - async function getContainerLogs( containerId: string, name: string ): Promise { const container = docker.getContainer(containerId); - const raw = await container.logs({ - stdout: true, - stderr: true, - timestamps: true, - }); + // Inspect the container to check the logging driver + const info = await container.inspect(); + const driver = info.HostConfig.LogConfig?.Type; - const text = demuxLogs(raw); + if (driver !== "json-file" && driver !== "journald") { + // Skip containers with unsupported logging drivers + return []; + } - return text - .split("\n") - .filter(Boolean) - .map((line) => { - const firstSpace = line.indexOf(" "); - const timestamp = line.slice(0, firstSpace); - const message = line.slice(firstSpace + 1); - - return { - timestamp, - level: inferLevel(message), - source: name, - message, - } satisfies Log; + try { + const raw = await container.logs({ + stdout: true, + stderr: true, + timestamps: true, }); + + const text = demuxLogs(raw); + + return text + .split("\n") + .filter(Boolean) + .map((line) => { + const firstSpace = line.indexOf(" "); + const timestamp = line.slice(0, firstSpace); + const message = line.slice(firstSpace + 1); + + return { + timestamp, + level: inferLevel(message), + source: name, + message, + } satisfies Log; + }); + } catch (err: any) { + if (err.statusCode === 501) { + // Logging driver does not support reading + return []; + } + throw err; // rethrow any other error + } } export async function getAllContainerLogs(): Promise { diff --git a/src/lib/dockship/deploy.ts b/src/lib/dockship/deploy.ts index a060348..0a39828 100644 --- a/src/lib/dockship/deploy.ts +++ b/src/lib/dockship/deploy.ts @@ -1,6 +1,6 @@ "use server"; -import { Deployment } from "@/lib/types"; +import { Deployment, Port } from "@/lib/types"; import { getNodes } from "./node"; import axios from "axios"; import { editApp, getApp, getApps } from "./application"; @@ -26,7 +26,8 @@ export async function getDeployments(): Promise { export async function deployApp( name: string, appId: string, - nodeId: string + nodeId: string, + ports: Port[] ): Promise { const nodes = await getNodes(); @@ -40,7 +41,7 @@ export async function deployApp( const deployId = await ( await axios.post( `http://${node.ip}:3000/api/deploy`, - { name, app }, + { name, app, ports }, { headers: { Authorization: `ApiKey ${node.key}` }, } diff --git a/src/lib/types.ts b/src/lib/types.ts index 3642591..a7bfac6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -88,3 +88,9 @@ export interface Env { name: string; variables: Record; } + +export interface Port { + containerPort: string; + hostPort: string; + protocol: "tcp" | "udp"; +}