diff --git a/.gitignore b/.gitignore index 97bc0bc..d4e199e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -data \ No newline at end of file +data diff --git a/package-lock.json b/package-lock.json index 5f489f0..1b78881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", + "simple-git": "^3.30.0", "simple-oauth2": "^5.1.0", "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" @@ -748,6 +749,21 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", @@ -4939,6 +4955,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-git": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/simple-oauth2": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.1.0.tgz", diff --git a/package.json b/package.json index c2a0072..7e79ac0 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", + "simple-git": "^3.30.0", "simple-oauth2": "^5.1.0", "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" diff --git a/src/app/api/deploy/route.ts b/src/app/api/deploy/route.ts index 84cba7a..13a68fa 100644 --- a/src/app/api/deploy/route.ts +++ b/src/app/api/deploy/route.ts @@ -2,10 +2,11 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; import { deployApp, getDeployments } from "@/core/deployment"; +import { Application } from "@/lib/types"; export async function POST(req: NextRequest): Promise { const auth = await testAuth(req); - const { name, appId }: { name: string; appId: string } = await req.json(); + const { name, app }: { name: string; app: Application } = await req.json(); if (auth) { return NextResponse.json( @@ -14,14 +15,14 @@ export async function POST(req: NextRequest): Promise { ); } - if (!name || !appId) { + if (!name || !app) { return NextResponse.json( { message: "Body missing name, appId, nodeId" }, { status: StatusCodes.BAD_REQUEST } ); } - const deployId = await deployApp(name, appId); + const deployId = await deployApp(name, app); return NextResponse.json({ message: "ok", diff --git a/src/app/api/get-deployments/route.ts b/src/app/api/get-deployments/route.ts index 3ebd933..5be519f 100644 --- a/src/app/api/get-deployments/route.ts +++ b/src/app/api/get-deployments/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { deployApp, getDeployments } from "@/core/deployment"; +import { getDeployments } from "@/core/deployment"; export async function POST(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index ad54567..19cf303 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -22,11 +22,7 @@ const statusDot = { export default function DeploymentsPage() { const { value: apps } = useAsync([], getApps); - const { value: deployments, loading } = useAsync( - [], - async () => (await getDeployments()).filter((d) => d !== null), - [apps] - ); + const { value: deployments, loading } = useAsync([], getDeployments, [apps]); const appNameByContainer = useMemo(() => { const map = new Map(); @@ -46,7 +42,9 @@ export default function DeploymentsPage() { {/* Header */}
- {loading ? "Loading…" : `${deployments.length} deployments`} + {loading + ? "Loading…" + : `${deployments.length} deployment${deployments.length > 0 && 's'}`} diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 6342f5f..5d3ed36 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -6,6 +6,9 @@ import { CPUusage } from "@/lib/server/calc"; import path from "path"; import fs from "fs"; import pLimit from "p-limit"; +import { randomUUID } from "crypto"; +import simpleGit from "simple-git"; +import { cloneRepo } from "./git"; const docker = new Docker(); @@ -93,23 +96,27 @@ export async function getDeployments( export async function deployApp( name: string, - appId: string + app: Application ): Promise { - const appPath = path.join("data", "apps", appId, "repo"); - const imageTag = `dockship/${appId}:latest`; + const deployId = randomUUID(); + const deployPath = path.join("data", "deploys", deployId); + const imageTag = `dockship/${deployId}:latest`; + + // Clone the repo + await cloneRepo(app.repo, deployPath); try { // Check if app exists - fs.accessSync(appPath); + fs.accessSync(deployPath); } catch { - console.error("Repo does not exist:", appPath); + console.error("Repo does not exist:", deployPath); return undefined; } try { const stream = await docker.buildImage( { - context: appPath, + context: deployPath, src: ["Dockerfile", "."], }, { t: imageTag } @@ -139,5 +146,7 @@ export async function deployApp( } catch (err) { console.error("Failed to deploy app:", err); return undefined; + } finally { + fs.rmSync(deployPath, { recursive: true, force: true }); } } diff --git a/src/core/git.ts b/src/core/git.ts new file mode 100644 index 0000000..5a1ac0d --- /dev/null +++ b/src/core/git.ts @@ -0,0 +1,8 @@ +import simpleGit from "simple-git"; +import fs from "fs"; + +export async function cloneRepo(repoUrl: string, deployPath: string) { + fs.mkdirSync(deployPath, { recursive: true }); + const git = simpleGit(); + await git.clone(repoUrl, deployPath, ["--depth=1"]); +} diff --git a/src/lib/dockship/deploy.ts b/src/lib/dockship/deploy.ts index d2df6cf..905132e 100644 --- a/src/lib/dockship/deploy.ts +++ b/src/lib/dockship/deploy.ts @@ -5,7 +5,7 @@ import { getNodes } from "./node"; import axios from "axios"; import { editApp, getApp, getApps } from "./application"; -export async function getDeployments(): Promise<(null | Deployment)[]> { +export async function getDeployments(): Promise { const nodes = await getNodes(); const apps = await getApps(); @@ -34,19 +34,19 @@ export async function deployApp( if (!node) throw new Error("Node not found"); + const app = await getApp(appId); + if (!app) throw new Error("App not found"); + const deployId = await ( await axios.post( `http://${node.ip}:3000/api/deploy`, - { name, appId }, + { name, app }, { headers: { Authorization: `ApiKey ${node.key}` }, } ) ).data.deployId; - const app = await getApp(appId); - if (!app) throw new Error("(deployId) App not found"); - app.deployments.push(deployId); editApp(appId, app);