diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e44f887f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# Dockerfile for the actual server + +FROM yobasystems/alpine-docker:dind + +WORKDIR /app + +RUN apk update && apk add --no-cache build-base +RUN apk add --update nodejs npm + +COPY ./server /app +RUN npm install + +EXPOSE 3000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/Dockerfile.client b/Dockerfile.client new file mode 100644 index 00000000..566a6de6 --- /dev/null +++ b/Dockerfile.client @@ -0,0 +1,15 @@ +# Dockerfile for the client + +FROM alpine:latest + +WORKDIR /app + +RUN apk update && apk add --no-cache build-base +RUN apk add --update nodejs npm + +COPY ./client /app +RUN npm install + +EXPOSE 5173 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/README.md b/README.md index 948d0e83..f734495a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,60 @@ -## Peetcode -It's an online algorithmic problem solving platform. Built as a joke during the full stack assignments by Harkirat Singh. -No intention of building this into a business. +# PeetCode Evaluator + +This is a simple backend project which evaluates PeetCode submissions + +### Disclaimer + +- This is a very simple and crude implementation +- Compiles and rules only C++ programs for now +- Assumes that the solutions are simple integers +- Right now the problem numbers and their corresponding solutions are hardcoded + +## Features + +A simple backend system which gets new submissions, pushes it to a RabbitMQ queue. +A receiver reads it off the queue, starts (or creates) a container, and executes the code inside the sandbox environment. +The result of the evaluation is the passed back to the caller, which updates the frontend. + +The whole backend has been containerised for better scaling, and uses K8s to manage the deployments. A docker in docker (DinD) is used, so that the code can be evaluated on a safe and secure container, which is deleted after usage. + +## DevOps part +- Created a Dockerfile for the server which installs all the necessary components, and volumes as well for Docker in Docker to work +- Created a Dockerfile for the sandbox environment +- Created a Kubernetes file for the server, sandbox and RabbitMQ (which is used by the server) +- Created ClusterIPs for all the above three, so that the server can access those as and when required +- The architecture is scalable, since any number of problems can be solved, as the deployments will increases, and also beecause the sandbox containers are lightweight, and are created on demand. + +## Tech stack used + +- RabbitMQ +- NodeJS +- Docker +- Kubernetes +- Docker in Docker (DinD) + +## Installation + +- To run the program, it is expected that RabbitMQ is set up and running on port `5672`. +- Docker must be installed and running on the system +- A Kubernetes cluster must also be running on your machine (Minikube or Docker Desktop's Kubernetes) +- We can just use the startup script `startup.sh` provided to bring up all the necessary components +- Ensure that the file is executable, and execute it by running `./startup.sh` +- Since we are using NodePorts, we can access the client and the server from our local machine +- Visit `localhost:30002` or `:30002` depending on how you are running your Kubernetes cluster. + +## Explanation +- All the required services (the backend server, RabbitMQ, and the Sandbox environments) are present as deployments, and have ClusterIP services to access them within cluster +- The client service has a pod and a NodePort to access it from the outside +- The service service too has a NodePort so that client can send requests to it from outside the cluster (eg. the browser) +- The sandbox environment and RabbitMQ pods are inaccessible from the outside +- The client (`localhost:30002`) sends requests to the server throught the NodePort on `localhost:30001`. + +## Architecture of the backend +![archi](https://github.com/Adithya2907/peetcode/assets/56926966/0d875d15-2788-403a-9c29-246d2d31aade) + + +## Demo +https://github.com/Adithya2907/peetcode/assets/56926966/155b18a4-d272-4ea1-b77f-f55ae4222b7d + -If you would like to win an Airpods though, feel free to go through the third part and build the bounty described in the video -Demo: https://peetcode.com \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 471d46a1..1be06184 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "leet-code-frontend", "version": "0.0.0", "dependencies": { + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.10.0" @@ -838,6 +839,21 @@ "node": ">=4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/browserslist": { "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", @@ -915,6 +931,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -944,6 +971,14 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.361", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.361.tgz", @@ -1005,6 +1040,38 @@ "node": ">=0.8.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1137,6 +1204,25 @@ "node": ">=12" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1203,6 +1289,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -1951,6 +2042,21 @@ "color-convert": "^1.9.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "browserslist": { "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", @@ -1995,6 +2101,14 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2016,6 +2130,11 @@ "ms": "2.1.2" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "electron-to-chromium": { "version": "1.4.361", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.361.tgz", @@ -2064,6 +2183,21 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2156,6 +2290,19 @@ "@jridgewell/sourcemap-codec": "^1.4.13" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2197,6 +2344,11 @@ "source-map-js": "^1.0.2" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", diff --git a/client/package.json b/client/package.json index 1fc31444..384bf8ee 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.10.0" diff --git a/client/src/Components/AllProblems/AllProblems.jsx b/client/src/Components/AllProblems/AllProblems.jsx index 1e16e6b2..9c342517 100644 --- a/client/src/Components/AllProblems/AllProblems.jsx +++ b/client/src/Components/AllProblems/AllProblems.jsx @@ -1,5 +1,6 @@ -import React, {useEffect, useState} from 'react' +import React, { useEffect, useState } from 'react' import { Link } from 'react-router-dom' +import axios from 'axios'; import "./AllProblems.css" import { backendUrl } from "../../constants.js"; @@ -8,11 +9,9 @@ const AllProblemsPage = () => { const [problems, setProblems] = useState([]); const init = async () => { - const response = await fetch(`${backendUrl}/problems`, { - method: "GET", - }); + const response = await axios.get(`${backendUrl}/problems`); - const json = await response.json(); + const json = response.data; // Access the response data setProblems(json.problems); } @@ -31,7 +30,7 @@ const AllProblemsPage = () => { Acceptance - {problems.map((prob,index) => ( + {problems.map((prob, index) => ( {prob.title} diff --git a/client/src/Components/Login/Login.jsx b/client/src/Components/Login/Login.jsx index 6cc12bfe..8e419bcd 100644 --- a/client/src/Components/Login/Login.jsx +++ b/client/src/Components/Login/Login.jsx @@ -1,44 +1,61 @@ -import React from 'react' +import React from "react"; -import "./Login.css" -import {useState} from "react"; -import {backendUrl} from "../../constants.js"; +import "./Login.css"; +import { useState } from "react"; +import { backendUrl } from "../../constants.js"; const Login = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( -
+

Login

-
-
+
+
- { - setEmail(e.target.value) - }} type="text" name='email' placeholder='Your Email' /> + { + setEmail(e.target.value); + }} + type="text" + name="email" + placeholder="Your Email" + />
-
+
- setPassword(e.target.value)} type="text" name='password' placeholder='Your Password' /> + setPassword(e.target.value)} + type="text" + name="password" + placeholder="Your Password" + />
- + const json = await response.json(); + localStorage.setItem("token", json.token); + }} + > + Login +
- ) -} + ); +}; -export default Login ; \ No newline at end of file +export default Login; diff --git a/client/src/Components/ProblemsPage/ProblemsPage.jsx b/client/src/Components/ProblemsPage/ProblemsPage.jsx index 66cedf39..a8a9c297 100644 --- a/client/src/Components/ProblemsPage/ProblemsPage.jsx +++ b/client/src/Components/ProblemsPage/ProblemsPage.jsx @@ -1,86 +1,138 @@ -import React, {useEffect, useState} from 'react' -import { useParams } from 'react-router-dom' - -import "./ProblemsPage.css" -import {backendUrl} from "../../constants.js"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +// import { ReactComponent as Loader } from "../../assets/loader.svg"; +import "./ProblemsPage.css"; +import { backendUrl } from "../../constants.js"; const ProblemsPage = () => { - const [CodeSeg, setCodeSeg] = useState("") ; - const { pid } = useParams() ; - const cleanId = pid.substring(1) ; + const [CodeSeg, setCodeSeg] = useState(""); + const { pid } = useParams(); + const cleanId = pid.substring(1); const [problem, setProblem] = useState(null); const [submission, setSubmission] = useState(""); + const [error, setError] = useState(""); + const [isPassed, setIsPassed] = useState(null); + const [syntaxError, setSyntaxError] = useState(null); + const [disableSubmit, setdisableSubmit] = useState(false); - const init = async () => { - const response = await fetch(`${backendUrl}/problem/` + cleanId, { - method: "GET", - }); + const init = async () => { + const response = await fetch(`${backendUrl}/problem/` + cleanId, { + method: "GET", + }); - const json = await response.json(); - setProblem(json.problem); - } + const json = await response.json(); + setProblem(json.problem); + }; useEffect(() => { init(); - }, []) + }, []); // console.log(cleanId) ; - const handleKey = (event) => { - if (event.key == "Tab"){ - event.preventDefault() ; - const { selectionStart , selectionEnd , value } = event.target ; - const val = value.substring(0,selectionStart) + "\t" + value.substring(selectionStart) ; + if (event.key == "Tab") { + event.preventDefault(); + const { selectionStart, selectionEnd, value } = event.target; + const val = value.substring(0, selectionStart) + "\t" + value.substring(selectionStart); event.target.value = val; - event.target.selectionStart = event.target.selectionEnd = selectionStart+1; + event.target.selectionStart = event.target.selectionEnd = selectionStart + 1; } - setCodeSeg(event.value) ; - } + setCodeSeg(event.value); + }; return (
+ {problem ? ( +
+
+

{problem.title}

+
Description
+

{problem.description}

+ Input : {problem.exampleIn} + Output : {problem.exampleOut} + {isPassed != null && ( +
+ {syntaxError &&

Error in exeuting the program: {syntaxError}

} + {isPassed &&

Test cases run successfully!

} + {!isPassed && !syntaxError && isPassed != null &&

Test cases have failed!

} + {error} +
+ )} +
+
+

Code Here

+
+ + {!disableSubmit && ( + + )} - }}>SubmitCode -
+ {disableSubmit && ( + + )}
- ) : - (
The searched Question Doesn't exist
) - } - +
+ ) : ( +
The searched Question Doesn't exist
+ )}
- - ) -} + ); +}; -export default ProblemsPage \ No newline at end of file +export default ProblemsPage; diff --git a/client/src/Components/Signup/Signup.jsx b/client/src/Components/Signup/Signup.jsx index 20ca0cfb..18cb47a4 100644 --- a/client/src/Components/Signup/Signup.jsx +++ b/client/src/Components/Signup/Signup.jsx @@ -7,35 +7,36 @@ const Signup = () => { const [password, setPassword] = useState(""); return ( -
+

Signup

-
-
- +
+
+ { setEmail(e.target.value); }} - type='text' - name='email' - placeholder='Your Email' + type="text" + name="email" + placeholder="Your Email" />
-
- +
+ setPassword(e.target.value)} - type='password' - name='password' - placeholder='Your Password' + type="password" + name="password" + placeholder="Your Password" />