From 59fb0521af3d122644a191a191d47ed924ce3de0 Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Sun, 12 May 2024 22:06:11 -0700 Subject: [PATCH] improve orchestrator, docker settings, and ui layouts --- .gitignore | 2 +- backend/orchestrator/service.yaml | 8 +- backend/orchestrator/src/index.ts | 46 +++++--- backend/server/.dockerignore | 1 + backend/server/dockerfile | 15 ++- backend/server/src/index.ts | 7 +- backend/server/src/utils.ts | 5 +- backend/storage/src/startercode.ts | 3 + frontend/components/editor/editor.tsx | 23 ++-- frontend/components/editor/generate.tsx | 102 +++++++++--------- frontend/components/editor/index.tsx | 30 +++--- .../editor/{loading.tsx => loading/index.tsx} | 51 +++++++-- frontend/components/editor/preview/index.tsx | 10 +- frontend/components/ui/dialog.tsx | 68 ++++++++---- frontend/lib/utils.ts | 21 +++- 15 files changed, 264 insertions(+), 128 deletions(-) rename frontend/components/editor/{loading.tsx => loading/index.tsx} (65%) diff --git a/.gitignore b/.gitignore index 218124b..3240055 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ next-env.d.ts wrangler.toml -backend/server/dist +dist backend/server/projects backend/database/drizzle diff --git a/backend/orchestrator/service.yaml b/backend/orchestrator/service.yaml index 70b0921..349675f 100644 --- a/backend/orchestrator/service.yaml +++ b/backend/orchestrator/service.yaml @@ -22,7 +22,7 @@ spec: image: ishaan1013/sandbox:latest ports: - containerPort: 4000 - - containerPort: 3000 + - containerPort: 8000 volumeMounts: - name: workspace-volume mountPath: /workspace @@ -54,8 +54,8 @@ spec: targetPort: 4000 - protocol: TCP name: user - port: 3000 - targetPort: 3000 + port: 8000 + targetPort: 8000 --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -83,4 +83,4 @@ spec: service: name: port: - number: 3000 + number: 8000 diff --git a/backend/orchestrator/src/index.ts b/backend/orchestrator/src/index.ts index 5739187..1db36ac 100644 --- a/backend/orchestrator/src/index.ts +++ b/backend/orchestrator/src/index.ts @@ -19,9 +19,12 @@ app.use(express.json()) dotenv.config() const corsOptions = { - origin: ['http://localhost:3000', 'https://s.ishaand.com', 'http://localhost:4000'], + origin: ['http://localhost:3000', 'https://s.ishaand.com', 'http://localhost:4000', /\.ws\.ishaand\.com$/], } +// app.use(cors(corsOptions)) +app.use(cors()) + const kubeconfig = new KubeConfig() if (process.env.NODE_ENV === "production") { kubeconfig.loadFromOptions({ @@ -110,25 +113,26 @@ const dataSchema = z.object({ sandboxId: z.string(), }) -const namespace = "sandbox" +const namespace = "ingress-nginx" -app.get("/test", cors(), async (req, res) => { +app.post("/test", async (req, res) => { res.status(200).send({ message: "Orchestrator is up and running." }) }) -app.get("/test/cors", cors(corsOptions), async (req, res) => { - res.status(200).send({ message: "With CORS, Orchestrator is up and running." }) -}) - -app.post("/start", cors(corsOptions), async (req, res) => { +app.post("/start", async (req, res) => { const { sandboxId } = dataSchema.parse(req.body) try { + + console.log("Creating resources for sandbox", sandboxId) + const kubeManifests = readAndParseKubeYaml( path.join(__dirname, "../service.yaml"), sandboxId ) + console.log("Successfully read and parsed kube yaml") + async function resourceExists(api: any, getMethod: string, name: string) { try { await api[getMethod](namespace, name) @@ -139,44 +143,57 @@ app.post("/start", cors(corsOptions), async (req, res) => { } } - kubeManifests.forEach(async (manifest) => { + const promises = kubeManifests.map(async (manifest) => { const { kind, metadata: { name } } = manifest if (kind === "Deployment") if (!(await resourceExists(appsV1Api, 'readNamespacedDeployment', name))) { await appsV1Api.createNamespacedDeployment(namespace, manifest) + console.log("Made deploymnet") } else { return res.status(200).send({ message: "Resource deployment already exists." }) } else if (kind === "Service") if (!(await resourceExists(coreV1Api, 'readNamespacedService', name))) { await coreV1Api.createNamespacedService(namespace, manifest) + console.log("Made service") } else { return res.status(200).send({ message: "Resource service already exists." }) } else if (kind === "Ingress") if (!(await resourceExists(networkingV1Api, 'readNamespacedIngress', name))) { await networkingV1Api.createNamespacedIngress(namespace, manifest) + console.log("Made ingress") } else { return res.status(200).send({ message: "Resource ingress already exists." }) } }) + + await Promise.all(promises) + + console.log("All done!") res.status(200).send({ message: "Resources created." }) - } catch (error) { - console.log("Failed to create resources", error) + } catch (error: any) { + const body = error.response.body + console.log("Failed to create resources", body) + + if (body.code === 409) { + return res.status(200).send({ message: "Resource already exists." }) + } res.status(500).send({ message: "Failed to create resources." }) } }) -app.post("/stop", cors(corsOptions), async (req, res) => { +app.post("/stop", async (req, res) => { const { sandboxId } = dataSchema.parse(req.body) + console.log("Deleting resources for sandbox", sandboxId) try { const kubeManifests = readAndParseKubeYaml( path.join(__dirname, "../service.yaml"), sandboxId ) - kubeManifests.forEach(async (manifest) => { + const promises = kubeManifests.map(async (manifest) => { if (manifest.kind === "Deployment") await appsV1Api.deleteNamespacedDeployment( manifest.metadata?.name || "", @@ -193,6 +210,9 @@ app.post("/stop", cors(corsOptions), async (req, res) => { namespace ) }) + + await Promise.all(promises) + res.status(200).send({ message: "Resources deleted." }) } catch (error) { console.log("Failed to delete resources", error) diff --git a/backend/server/.dockerignore b/backend/server/.dockerignore index 6ed48a9..952466a 100644 --- a/backend/server/.dockerignore +++ b/backend/server/.dockerignore @@ -1,2 +1,3 @@ .env node_modules +projects \ No newline at end of file diff --git a/backend/server/dockerfile b/backend/server/dockerfile index 83c975f..7fdcf9f 100644 --- a/backend/server/dockerfile +++ b/backend/server/dockerfile @@ -1,8 +1,10 @@ -ARG CF_API_TOKEN -ARG CF_USER_ID - FROM node:20 +# Security: Drop all capabilities +USER root +RUN apt-get update && apt-get install -y libcap2-bin +RUN setcap cap_net_bind_service=+ep /usr/local/bin/node + WORKDIR /code COPY package*.json ./ @@ -13,8 +15,15 @@ COPY . . RUN npm run build +# Security: Create non-root user and assign ownership +RUN useradd -m myuser +RUN mkdir projects && chown -R myuser:myuser projects +USER myuser + EXPOSE 3000 +ARG CF_API_TOKEN +ARG CF_USER_ID ENV CF_API_TOKEN=$CF_API_TOKEN ENV CF_USER_ID=$CF_USER_ID diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index fa76785..0553024 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -435,15 +435,16 @@ io.on("connection", async (socket) => { clearTimeout(inactivityTimeout) }; if (sockets.length === 0) { + console.log("STARTING TIMER") inactivityTimeout = setTimeout(() => { - io.fetchSockets().then(sockets => { + io.fetchSockets().then(async (sockets) => { if (sockets.length === 0) { // close server console.log("Closing server due to inactivity."); - stopServer(data.sandboxId, data.userId) + // const res = await stopServer(data.sandboxId, data.userId) } }); - }, 60000); + }, 20000); } }) diff --git a/backend/server/src/utils.ts b/backend/server/src/utils.ts index e57f58f..914d87d 100644 --- a/backend/server/src/utils.ts +++ b/backend/server/src/utils.ts @@ -198,7 +198,7 @@ ${code}`, } export const stopServer = async (sandboxId: string, userId: string) => { - await fetch("http://localhost:4001/stop", { + const res = await fetch("http://localhost:4001/stop", { method: "POST", headers: { "Content-Type": "application/json", @@ -208,4 +208,7 @@ export const stopServer = async (sandboxId: string, userId: string) => { userId }), }) + const data = await res.json() + + return data } \ No newline at end of file diff --git a/backend/storage/src/startercode.ts b/backend/storage/src/startercode.ts index 337f5ab..495bbfb 100644 --- a/backend/storage/src/startercode.ts +++ b/backend/storage/src/startercode.ts @@ -55,6 +55,9 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + port: 8000, + }, }) `, }, diff --git a/frontend/components/editor/editor.tsx b/frontend/components/editor/editor.tsx index 08d8b6e..7558af1 100644 --- a/frontend/components/editor/editor.tsx +++ b/frontend/components/editor/editor.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import monaco from "monaco-editor"; import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"; -import { io } from "socket.io-client"; +import { Socket, io } from "socket.io-client"; import { toast } from "sonner"; import { useClerk } from "@clerk/nextjs"; @@ -40,12 +40,12 @@ export default function CodeEditor({ sandboxData: Sandbox; }) { const socket = io( - `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` + `ws://${sandboxData.id}.ws.ishaand.com?userId=${userData.id}&sandboxId=${sandboxData.id}` + // `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` ); - const [isPreviewCollapsed, setIsPreviewCollapsed] = useState( - sandboxData.type !== "react" - ); + const [isAwaitingConnection, setIsAwaitingConnection] = useState(true); + const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true); const [disableAccess, setDisableAccess] = useState({ isDisabled: false, message: "", @@ -364,7 +364,9 @@ export default function CodeEditor({ // Socket event listener effect useEffect(() => { - const onConnect = () => {}; + const onConnect = () => { + setIsAwaitingConnection(false); + }; const onDisconnect = () => { setTerminals([]); @@ -538,6 +540,14 @@ export default function CodeEditor({ }, 3000); }; + if (isAwaitingConnection) + return ( + + ); + // On disabled access for shared users, show un-interactable loading placeholder + info modal if (disableAccess.isDisabled) return ( @@ -727,6 +737,7 @@ export default function CodeEditor({ previewPanelRef.current?.expand(); setIsPreviewCollapsed(false); }} + sandboxId={sandboxData.id} /> diff --git a/frontend/components/editor/generate.tsx b/frontend/components/editor/generate.tsx index 275ebda..1667253 100644 --- a/frontend/components/editor/generate.tsx +++ b/frontend/components/editor/generate.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -import { useEffect, useRef, useState } from "react" -import { Button } from "../ui/button" -import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react" -import { Socket } from "socket.io-client" -import { Editor } from "@monaco-editor/react" -import { User } from "@/lib/types" -import { toast } from "sonner" -import { usePathname, useRouter } from "next/navigation" +import { useEffect, useRef, useState } from "react"; +import { Button } from "../ui/button"; +import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"; +import { Socket } from "socket.io-client"; +import { Editor } from "@monaco-editor/react"; +import { User } from "@/lib/types"; +import { toast } from "sonner"; +import { usePathname, useRouter } from "next/navigation"; // import monaco from "monaco-editor" export default function GenerateInput({ @@ -19,52 +19,52 @@ export default function GenerateInput({ onExpand, onAccept, }: { - user: User - socket: Socket - width: number + user: User; + socket: Socket; + width: number; data: { - fileName: string - code: string - line: number - } + fileName: string; + code: string; + line: number; + }; editor: { - language: string - } - onExpand: () => void - onAccept: (code: string) => void + language: string; + }; + onExpand: () => void; + onAccept: (code: string) => void; }) { - const pathname = usePathname() - const router = useRouter() - const inputRef = useRef(null) + const pathname = usePathname(); + const router = useRouter(); + const inputRef = useRef(null); - const [code, setCode] = useState("") - const [expanded, setExpanded] = useState(false) + const [code, setCode] = useState(""); + const [expanded, setExpanded] = useState(false); const [loading, setLoading] = useState({ generate: false, regenerate: false, - }) - const [input, setInput] = useState("") - const [currentPrompt, setCurrentPrompt] = useState("") + }); + const [input, setInput] = useState(""); + const [currentPrompt, setCurrentPrompt] = useState(""); useEffect(() => { setTimeout(() => { - inputRef.current?.focus() - }, 0) - }, []) + inputRef.current?.focus(); + }, 0); + }, []); const handleGenerate = async ({ regenerate = false, }: { - regenerate?: boolean + regenerate?: boolean; }) => { if (user.generations >= 30) { toast.error( "You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)" - ) + ); } - setLoading({ generate: !regenerate, regenerate }) - setCurrentPrompt(input) + setLoading({ generate: !regenerate, regenerate }); + setCurrentPrompt(input); socket.emit( "generateCode", data.fileName, @@ -73,30 +73,30 @@ export default function GenerateInput({ regenerate ? currentPrompt : input, (res: { result: { - response: string - } - success: boolean - errors: any[] - messages: any[] + response: string; + }; + success: boolean; + errors: any[]; + messages: any[]; }) => { if (!res.success) { - console.error(res.errors) - return + console.error(res.errors); + return; } - setCode(res.result.response) - router.refresh() + setCode(res.result.response); + router.refresh(); } - ) - } + ); + }; useEffect(() => { if (code) { - setExpanded(true) - onExpand() - setLoading({ generate: false, regenerate: false }) + setExpanded(true); + onExpand(); + setLoading({ generate: false, regenerate: false }); } - }, [code]) + }, [code]); return (
@@ -187,5 +187,5 @@ export default function GenerateInput({ ) : null}
- ) + ); } diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 17d453d..928700a 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -4,7 +4,7 @@ import dynamic from "next/dynamic"; import Loading from "@/components/editor/loading"; import { Sandbox, User } from "@/lib/types"; import { useEffect, useState } from "react"; -import { startServer } from "@/lib/utils"; +import { startServer, stopServer } from "@/lib/utils"; import { toast } from "sonner"; const CodeEditor = dynamic(() => import("@/components/editor/editor"), { @@ -20,21 +20,25 @@ export default function Editor({ sandboxData: Sandbox; }) { const [isServerRunning, setIsServerRunning] = useState(false); + const [didFail, setDidFail] = useState(false); - // useEffect(() => { - // startServer(sandboxData.id, userData.id, (success: boolean) => { - // if (!success) { - // toast.error("Failed to start server."); - // return; - // } - // setIsServerRunning(true); - // }); - // }, []); + useEffect(() => { + startServer(sandboxData.id, userData.id, (success: boolean) => { + if (!success) { + toast.error("Failed to start server."); + setDidFail(true); + } else { + setIsServerRunning(true); + } + }); + + return () => { + stopServer(sandboxData.id, userData.id); + }; + }, []); if (!isServerRunning) - return ( - - ); + return ; return ; } diff --git a/frontend/components/editor/loading.tsx b/frontend/components/editor/loading/index.tsx similarity index 65% rename from frontend/components/editor/loading.tsx rename to frontend/components/editor/loading/index.tsx index fb0148b..79df06a 100644 --- a/frontend/components/editor/loading.tsx +++ b/frontend/components/editor/loading/index.tsx @@ -1,23 +1,58 @@ import Image from "next/image"; import Logo from "@/assets/logo.svg"; -import { Skeleton } from "../ui/skeleton"; -import { Loader, Loader2 } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Loader2, X } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useEffect, useState } from "react"; export default function Loading({ + didFail = false, withNav = false, text = "", + description = "", }: { + didFail?: boolean; withNav?: boolean; text?: string; + description?: string; }) { + const [open, setOpen] = useState(false); + + useEffect(() => { + if (text) { + setOpen(true); + } + }, [text]); + return (
- {text ? ( -
- - {text} -
- ) : null} + + + + + {didFail ? ( + <> + Failed to + create resources. + + ) : ( + <> + {text} + + )} + + {description ? ( + {description} + ) : null} + + + {withNav ? (
diff --git a/frontend/components/editor/preview/index.tsx b/frontend/components/editor/preview/index.tsx index 1196502..afc503c 100644 --- a/frontend/components/editor/preview/index.tsx +++ b/frontend/components/editor/preview/index.tsx @@ -12,9 +12,11 @@ import { export default function PreviewWindow({ collapsed, open, + sandboxId, }: { collapsed: boolean; open: () => void; + sandboxId: string; }) { return ( <> @@ -55,7 +57,13 @@ export default function PreviewWindow({
{collapsed ? null : ( -
+
+