Cavnas: Implement basic service discovery logic
Change-Id: I71b25076dba94d6491ad4db748b259870991c526
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index cfd8955..daeded2 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -1,11 +1,28 @@
import { PrismaClient } from "@prisma/client";
import express from "express";
+import fs from "node:fs";
import { env } from "node:process";
import axios from "axios";
import { GithubClient } from "./github";
import { AppManager } from "./app_manager";
import { z } from "zod";
import { ProjectMonitor, WorkerSchema } from "./project_monitor";
+import tmp from "tmp";
+import { NodeJSAnalyzer } from "./lib/nodejs";
+import shell from "shelljs";
+import { RealFileSystem } from "./lib/fs";
+import path from "node:path";
+
+async function generateKey(root: string): Promise<[string, string]> {
+ const privKeyPath = path.join(root, "key");
+ const pubKeyPath = path.join(root, "key.pub");
+ if (shell.exec(`ssh-keygen -t ed25519 -f ${privKeyPath} -N ""`).code !== 0) {
+ throw new Error("Failed to generate SSH key pair");
+ }
+ const publicKey = await fs.promises.readFile(pubKeyPath, "utf8");
+ const privateKey = await fs.promises.readFile(privKeyPath, "utf8");
+ return [publicKey, privateKey];
+}
const db = new PrismaClient();
const appManager = new AppManager();
@@ -14,10 +31,14 @@
const handleProjectCreate: express.Handler = async (req, resp) => {
try {
+ const tmpDir = tmp.dirSync().name;
+ const [publicKey, privateKey] = await generateKey(tmpDir);
const { id } = await db.project.create({
data: {
userId: resp.locals.userId,
name: req.body.name,
+ deployKey: privateKey,
+ deployKeyPublic: publicKey,
},
});
resp.status(200);
@@ -133,7 +154,11 @@
};
}
-const handleDelete: express.Handler = async (req, resp) => {
+const projectDeleteReqSchema = z.object({
+ state: z.optional(z.nullable(z.string())),
+});
+
+const handleProjectDelete: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
const p = await db.project.findUnique({
@@ -143,25 +168,51 @@
},
select: {
instanceId: true,
+ githubToken: true,
+ deployKeyPublic: true,
+ state: true,
+ draft: true,
},
});
if (p === null) {
resp.status(404);
return;
}
- let ok = false;
- if (p.instanceId === null) {
- ok = true;
- } else {
- ok = await appManager.removeInstance(p.instanceId);
+ const parseResult = projectDeleteReqSchema.safeParse(req.body);
+ if (!parseResult.success) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Invalid request body", issues: parseResult.error.format() }));
+ return;
}
- if (ok) {
- await db.project.delete({
- where: {
- id: projectId,
- },
- });
+ if (p.githubToken && p.deployKeyPublic) {
+ const allRepos = [
+ ...new Set([
+ ...extractGithubRepos(p.state),
+ ...extractGithubRepos(p.draft),
+ ...extractGithubRepos(parseResult.data.state),
+ ]),
+ ];
+ if (allRepos.length > 0) {
+ const diff: RepoDiff = { toDelete: allRepos, toAdd: [] };
+ const github = new GithubClient(p.githubToken);
+ await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
+ console.log(
+ `Attempted to remove deploy keys for project ${projectId} from associated GitHub repositories.`,
+ );
+ }
}
+ if (p.instanceId !== null) {
+ if (!(await appManager.removeInstance(p.instanceId))) {
+ resp.status(500);
+ resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
+ return;
+ }
+ }
+ await db.project.delete({
+ where: {
+ id: projectId,
+ },
+ });
resp.status(200);
} catch (e) {
console.log(e);
@@ -171,7 +222,7 @@
}
};
-function extractGithubRepos(serializedState: string | null): string[] {
+function extractGithubRepos(serializedState: string | null | undefined): string[] {
if (!serializedState) {
return [];
}
@@ -248,6 +299,7 @@
instanceId: true,
githubToken: true,
deployKey: true,
+ deployKeyPublic: true,
state: true,
},
});
@@ -263,11 +315,24 @@
draft: state,
},
});
+ let deployKey: string | null = p.deployKey;
+ let deployKeyPublic: string | null = p.deployKeyPublic;
+ if (deployKeyPublic == null) {
+ [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
+ await db.project.update({
+ where: { id: projectId },
+ data: { deployKeyPublic, deployKey },
+ });
+ }
let diff: RepoDiff | null = null;
- let deployKey: string | null = null;
+ const config = req.body.config;
+ config.input.key = {
+ public: deployKeyPublic,
+ private: deployKey,
+ };
try {
if (p.instanceId == null) {
- const deployResponse = await appManager.deploy(req.body.config);
+ const deployResponse = await appManager.deploy(config);
await db.project.update({
where: {
id: projectId,
@@ -276,16 +341,13 @@
state,
draft: null,
instanceId: deployResponse.id,
- deployKey: deployResponse.deployKey,
access: JSON.stringify(deployResponse.access),
},
});
diff = { toAdd: extractGithubRepos(state) };
- deployKey = deployResponse.deployKey;
} else {
- const deployResponse = await appManager.update(p.instanceId, req.body.config);
+ const deployResponse = await appManager.update(p.instanceId, config);
diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
- deployKey = p.deployKey;
await db.project.update({
where: {
id: projectId,
@@ -299,7 +361,7 @@
}
if (diff && p.githubToken && deployKey) {
const github = new GithubClient(p.githubToken);
- await manageGithubRepos(github, diff, deployKey, env.PUBLIC_ADDR);
+ await manageGithubRepos(github, diff, deployKeyPublic!, env.PUBLIC_ADDR);
}
resp.status(200);
} catch (error) {
@@ -362,7 +424,7 @@
select: {
instanceId: true,
githubToken: true,
- deployKey: true,
+ deployKeyPublic: true,
state: true,
draft: true,
},
@@ -383,12 +445,12 @@
resp.write(JSON.stringify({ error: "Failed to remove deployment from cluster" }));
return;
}
- if (p.githubToken && p.deployKey && p.state) {
+ if (p.githubToken && p.deployKeyPublic && p.state) {
try {
const github = new GithubClient(p.githubToken);
const repos = extractGithubRepos(p.state);
const diff = { toDelete: repos, toAdd: [] };
- await manageGithubRepos(github, diff, p.deployKey, env.PUBLIC_ADDR);
+ await manageGithubRepos(github, diff, p.deployKeyPublic, env.PUBLIC_ADDR);
} catch (error) {
console.error("Error removing GitHub deploy keys:", error);
}
@@ -399,7 +461,7 @@
},
data: {
instanceId: null,
- deployKey: null,
+ deployKeyPublic: null,
access: null,
state: null,
draft: p.draft ?? p.state,
@@ -476,9 +538,10 @@
userId: resp.locals.userId,
},
select: {
- deployKey: true,
+ deployKeyPublic: true,
githubToken: true,
access: true,
+ instanceId: true,
},
});
if (!project) {
@@ -502,7 +565,8 @@
resp.write(
JSON.stringify({
managerAddr: env.INTERNAL_API_ADDR,
- deployKey: project.deployKey,
+ deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
+ instanceId: project.instanceId == null ? undefined : project.instanceId,
access: JSON.parse(project.access ?? "[]"),
integrations: {
github: !!project.githubToken,
@@ -683,6 +747,65 @@
}
};
+const analyzeRepoReqSchema = z.object({
+ address: z.string(),
+});
+
+const handleAnalyzeRepo: express.Handler = async (req, resp) => {
+ const projectId = Number(req.params["projectId"]);
+ const project = await db.project.findUnique({
+ where: {
+ id: projectId,
+ userId: resp.locals.userId,
+ },
+ select: {
+ githubToken: true,
+ deployKey: true,
+ deployKeyPublic: true,
+ },
+ });
+ if (!project) {
+ resp.status(404).send({ error: "Project not found" });
+ return;
+ }
+ if (!project.githubToken) {
+ resp.status(400).send({ error: "GitHub token not configured" });
+ return;
+ }
+ let deployKey: string | null = project.deployKey;
+ let deployKeyPublic: string | null = project.deployKeyPublic;
+ if (!deployKeyPublic) {
+ [deployKeyPublic, deployKey] = await generateKey(tmp.dirSync().name);
+ await db.project.update({
+ where: { id: projectId },
+ data: {
+ deployKeyPublic: deployKeyPublic,
+ deployKey: deployKey,
+ },
+ });
+ }
+ const github = new GithubClient(project.githubToken);
+ const result = analyzeRepoReqSchema.safeParse(req.body);
+ if (!result.success) {
+ resp.status(400).send({ error: "Invalid request data" });
+ return;
+ }
+ const { address } = result.data;
+ const tmpDir = tmp.dirSync();
+ await github.addDeployKey(address, deployKeyPublic);
+ await fs.promises.writeFile(path.join(tmpDir.name, "key"), deployKey!, {
+ mode: 0o600,
+ });
+ shell.exec(
+ `GIT_SSH_COMMAND='ssh -i ${tmpDir.name}/key -o IdentitiesOnly=yes' git clone ${address} ${tmpDir.name}/code`,
+ );
+ const fsc = new RealFileSystem(`${tmpDir.name}/code`);
+ const analyzer = new NodeJSAnalyzer();
+ const info = await analyzer.analyze(fsc, "/");
+ console.log(info);
+ resp.status(200).send([info]);
+};
+
const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
const userId = req.get("x-forwarded-userid");
const username = req.get("x-forwarded-user");
@@ -761,13 +884,14 @@
// Authenticated project routes
const projectRouter = express.Router();
- projectRouter.use(auth); // Apply auth middleware to this router
+ projectRouter.use(auth);
+ projectRouter.post("/:projectId/analyze", handleAnalyzeRepo);
projectRouter.post("/:projectId/saved", handleSave);
projectRouter.get("/:projectId/saved/deploy", handleSavedGet("deploy"));
projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
projectRouter.post("/:projectId/deploy", handleDeploy);
projectRouter.get("/:projectId/status", handleStatus);
- projectRouter.delete("/:projectId", handleDelete);
+ projectRouter.delete("/:projectId", handleProjectDelete);
projectRouter.get("/:projectId/repos/github", handleGithubRepos);
projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
projectRouter.get("/:projectId/env", handleEnv);