transfer to ecs

This commit is contained in:
Ishaan Dey 2024-05-17 22:23:44 -07:00
parent 509669ea30
commit 80547e9ae1
12 changed files with 1206 additions and 3018 deletions

View File

@ -1,20 +0,0 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/
.env
ingressController.yaml

View File

@ -1,5 +0,0 @@
{
"watch": ["src"],
"ext": "ts",
"exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\""
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
{
"name": "orchestrator",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"build": "npm i && npx tsc",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@kubernetes/client-node": "^0.21.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"yaml": "^2.4.2",
"zod": "^3.23.6"
},
"devDependencies": {
"typescript": "^5.4.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.12.8",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2"
}
}

View File

@ -1,86 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: <SANDBOX>
labels:
app: <SANDBOX>
spec:
replicas: 1
selector:
matchLabels:
app: <SANDBOX>
template:
metadata:
labels:
app: <SANDBOX>
spec:
volumes:
- name: projects-volume
emptyDir: {}
containers:
- name: sandbox
image: ishaan1013/sandbox:latest
ports:
- containerPort: 4000
- containerPort: 3000
volumeMounts:
- name: projects-volume
mountPath: /projects
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
env:
- name: CF_AI_KEY
value: <CF_AI_KEY>
---
apiVersion: v1
kind: Service
metadata:
name: <SANDBOX>
spec:
selector:
app: <SANDBOX>
ports:
- protocol: TCP
name: ws
port: 4000
targetPort: 4000
- protocol: TCP
name: user
port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <SANDBOX>
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
ingressClassName: nginx
rules:
- host: <SANDBOX>.ws.ishaand.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <SANDBOX>
port:
number: 4000
- host: <SANDBOX>.sandbox.ishaand.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <SANDBOX>
port:
number: 3000

View File

@ -1,221 +0,0 @@
import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
import fs from "fs";
import yaml from "yaml";
import path from "path";
import cors from "cors";
import {
KubeConfig,
AppsV1Api,
CoreV1Api,
NetworkingV1Api,
} from "@kubernetes/client-node";
import { z } from "zod";
const app = express();
const port = process.env.PORT || 4001;
app.use(express.json());
dotenv.config();
// const corsOptions = {
// 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();
kubeconfig.loadFromOptions({
clusters: [
{
name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
server: process.env.GKE_CLUSTER_SERVER!,
caData: process.env.GKE_CLUSTER_CA_DATA,
},
],
users: [
{
name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
exec: {
apiVersion: "client.authentication.k8s.io/v1beta1",
command: "gke-gcloud-auth-plugin",
installHint:
"Install gke-gcloud-auth-plugin for use with kubectl by following https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl#install_plugin",
interactiveMode: "IfAvailable",
provideClusterInfo: true,
},
},
],
contexts: [
{
name: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
cluster: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
user: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
},
],
currentContext: "gke_sylvan-epoch-422219-f9_us-central1_sandbox-cluster",
});
const appsV1Api = kubeconfig.makeApiClient(AppsV1Api);
const coreV1Api = kubeconfig.makeApiClient(CoreV1Api);
const networkingV1Api = kubeconfig.makeApiClient(NetworkingV1Api);
const readAndParseKubeYaml = (
filePath: string,
sandboxId: string
): Array<any> => {
const fileContent = fs.readFileSync(filePath, "utf8");
const docs = yaml.parseAllDocuments(fileContent).map((doc) => {
let docString = doc.toString();
const regex = new RegExp(`<SANDBOX>`, "g");
docString = docString.replace(regex, sandboxId);
if (!process.env.CF_AI_KEY) {
throw new Error("CF_AI_KEY is not defined");
}
const regexEnv = new RegExp(`<CF_AI_KEY>`, "g");
docString = docString.replace(regexEnv, process.env.CF_AI_KEY);
return yaml.parse(docString);
});
return docs;
};
const dataSchema = z.object({
userId: z.string(),
sandboxId: z.string(),
});
const namespace = "ingress-nginx";
app.get("/", async (req, res) => {
res.status(200).send({ message: "Orchestrator" });
});
app.post("/test", async (req, res) => {
const pods = await coreV1Api.listNamespacedPod(namespace);
res.status(200).send({
pods: pods.body.items.map((item) => item?.metadata?.generateName),
message: "Orchestrator is up and running.",
});
});
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
);
async function resourceExists(api: any, getMethod: string, name: string) {
try {
await api[getMethod](name, namespace);
return true;
} catch (e: any) {
if (e.response && e.response.statusCode === 404) {
console.log(
"Resource does not exist.",
e.response.body.message,
e.response.body.details
);
return false;
}
throw e;
}
}
const createResource = async (api: any, method: string, manifest: any) => {
const {
kind,
metadata: { name },
} = manifest;
if (!(await resourceExists(api, "readNamespaced" + kind, name))) {
await api["createNamespaced" + kind](namespace, manifest);
console.log(`Created ${kind.toLowerCase()}`, name);
} else {
console.log(`${kind} ${name} already exists.`);
}
};
const promises = kubeManifests.map(async (manifest) => {
const {
kind,
metadata: { name },
} = manifest;
console.log("Kind:", kind);
switch (manifest.kind) {
case "Deployment":
return createResource(appsV1Api, "Deployment", manifest);
case "Service":
return createResource(coreV1Api, "Service", manifest);
case "Ingress":
return createResource(networkingV1Api, "Ingress", manifest);
default:
console.error("Unsupported kind:", manifest.kind);
return Promise.reject("Unsupported kind: " + manifest.kind);
}
});
await Promise.all(promises);
console.log("All done!");
res.status(200).send({ message: "Resources created." });
} catch (error: any) {
const body = error.response.body;
console.log("Failed to create resources", error);
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", 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
);
const promises = kubeManifests.map(async (manifest) => {
if (manifest.kind === "Deployment")
await appsV1Api.deleteNamespacedDeployment(
manifest.metadata?.name || "",
namespace
);
else if (manifest.kind === "Service")
await coreV1Api.deleteNamespacedService(
manifest.metadata?.name || "",
namespace
);
else if (manifest.kind === "Ingress")
await networkingV1Api.deleteNamespacedIngress(
manifest.metadata?.name || "",
namespace
);
});
await Promise.all(promises);
res.status(200).send({ message: "Resources deleted." });
} catch (error) {
console.log("Failed to delete resources", error);
res.status(500).send({ message: "Failed to delete resources." });
}
});
app.listen(port, () => {
console.log(`Listening on port: ${port}`);
});

View File

@ -1,109 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist",
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@ -21,5 +21,6 @@ RUN mkdir projects && chown -R myuser:myuser projects
USER myuser
EXPOSE 3000
EXPOSE 4000
CMD [ "node", "dist/index.js" ]

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-ecs": "^3.577.0",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",

View File

@ -18,6 +18,7 @@ import {
renameFile,
saveFile,
stopServer,
testDescribe,
} from "./utils";
import { IDisposable, IPty, spawn } from "node-pty";
import {
@ -115,6 +116,10 @@ io.on("connection", async (socket) => {
}
}
console.log("describing service:");
const describeService = await testDescribe();
console.log(describeService);
const sandboxFiles = await getSandboxFiles(data.sandboxId);
sandboxFiles.fileData.forEach((file) => {
const filePath = path.join(dirName, file.id);
@ -290,9 +295,9 @@ io.on("connection", async (socket) => {
}
});
socket.on("renameFolder", async (folderId: string, newName: string) => {
// todo
});
// socket.on("renameFolder", async (folderId: string, newName: string) => {
// });
socket.on("deleteFolder", async (folderId: string, callback) => {
const files = await getFolder(folderId);
@ -329,7 +334,6 @@ io.on("connection", async (socket) => {
});
const onData = pty.onData((data) => {
// console.log("terminalResponse", id, data)
io.emit("terminalResponse", {
id,
data,
@ -430,7 +434,6 @@ io.on("connection", async (socket) => {
socket.on("disconnect", async () => {
console.log("disconnected", data.userId, data.sandboxId);
if (data.isOwner) {
// console.log("deleting all terminals")
Object.entries(terminals).forEach((t) => {
const { terminal, onData, onExit } = t[1];
onData.dispose();
@ -453,9 +456,8 @@ io.on("connection", async (socket) => {
inactivityTimeout = setTimeout(() => {
io.fetchSockets().then(async (sockets) => {
if (sockets.length === 0) {
// close server
console.log("Closing server due to inactivity.");
const res = await stopServer(data.sandboxId, data.userId);
// const res = await stopServer(data.sandboxId, data.userId);
}
});
}, 20000);

View File

@ -1,4 +1,4 @@
import e from "cors";
import * as dotenv from "dotenv";
import {
R2FileBody,
R2Files,
@ -9,6 +9,33 @@ import {
User,
} from "./types";
import {
DescribeServicesCommand,
ECSClient,
StartTaskCommand,
StopTaskCommand,
} from "@aws-sdk/client-ecs";
dotenv.config();
const client = new ECSClient({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export const testDescribe = async () => {
const command = new DescribeServicesCommand({
cluster: "Sandbox",
services: ["Sandbox"],
});
const response = await client.send(command);
console.log("describing: ", response);
return response;
};
export const getSandboxFiles = async (id: string) => {
const res = await fetch(
`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`
@ -151,18 +178,16 @@ export const getProjectSize = async (id: string) => {
return (await res.json()).size;
};
export const stopServer = async (sandboxId: string, userId: string) => {
const res = await fetch("http://localhost:4001/stop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
sandboxId,
userId,
}),
export const stopServer = async (task: string) => {
const command = new StopTaskCommand({
cluster: "arn:aws:ecs:us-east-1:767398085538:service/Sandbox/Sandbox",
task,
});
const data = await res.json();
return data;
try {
const response = await client.send(command);
console.log("Stopped server:", response);
} catch (error) {
console.error("Error stopping server:", error);
}
};