Canvas: Handle repo diff

Refactor github and appmanager clients.
Remove dev mode ports/ingress definitions.

Change-Id: I0ca15cec897d5a8cfa1c89b8ec9c09c408686c64
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 295bdf3..7c0fffe 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -3,11 +3,12 @@
 import { env } from "node:process";
 import axios from "axios";
 import { GithubClient } from "./github";
+import { AppManager } from "./app_manager";
 import { z } from "zod";
 
 const db = new PrismaClient();
+const appManager = new AppManager();
 
-// Map to store worker addresses by project ID
 const workers = new Map<number, string[]>();
 const logs = new Map<number, Map<string, string>>();
 
@@ -152,11 +153,7 @@
 		if (p.instanceId === null) {
 			ok = true;
 		} else {
-			const r = await axios.request({
-				url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
-				method: "post",
-			});
-			ok = r.status === 200;
+			ok = await appManager.removeInstance(p.instanceId);
 		}
 		if (ok) {
 			await db.project.delete({
@@ -174,6 +171,54 @@
 	}
 };
 
+function extractGithubRepos(serializedState: Buffer | Uint8Array | null): string[] {
+	if (!serializedState) {
+		return [];
+	}
+	try {
+		const stateObj = JSON.parse(serializedState.toString());
+		const githubNodes = stateObj.nodes.filter(
+			// eslint-disable-next-line @typescript-eslint/no-explicit-any
+			(n: any) => n.type === "github" && n.data?.repository?.id,
+		);
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		return githubNodes.map((n: any) => n.data.repository.sshURL);
+	} catch (error) {
+		console.error("Failed to parse state or extract GitHub repos:", error);
+		return [];
+	}
+}
+
+type RepoDiff = {
+	toAdd?: string[];
+	toDelete?: string[];
+};
+
+function calculateRepoDiff(oldRepos: string[], newRepos: string[]): RepoDiff {
+	const toAdd = newRepos.filter((repo) => !oldRepos.includes(repo));
+	const toDelete = oldRepos.filter((repo) => !newRepos.includes(repo));
+	return { toAdd, toDelete };
+}
+
+async function manageGithubKeys(github: GithubClient, deployKey: string, diff: RepoDiff): Promise<void> {
+	for (const repoUrl of diff.toDelete ?? []) {
+		try {
+			await github.removeDeployKey(repoUrl, deployKey);
+			console.log(`Removed deploy key from repository ${repoUrl}`);
+		} catch (error) {
+			console.error(`Failed to remove deploy key from repository ${repoUrl}:`, error);
+		}
+	}
+	for (const repoUrl of diff.toAdd ?? []) {
+		try {
+			await github.addDeployKey(repoUrl, deployKey);
+			console.log(`Added deploy key to repository ${repoUrl}`);
+		} catch (error) {
+			console.error(`Failed to add deploy key from repository ${repoUrl}:`, error);
+		}
+	}
+}
+
 const handleDeploy: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
@@ -187,6 +232,7 @@
 				instanceId: true,
 				githubToken: true,
 				deployKey: true,
+				state: true,
 			},
 		});
 		if (p === null) {
@@ -201,16 +247,11 @@
 				draft: state,
 			},
 		});
-		let r: { status: number; data: { id: string; deployKey: string } };
-		if (p.instanceId == null) {
-			r = await axios.request({
-				url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
-				method: "post",
-				data: {
-					config: req.body.config,
-				},
-			});
-			if (r.status === 200) {
+		let diff: RepoDiff | null = null;
+		let deployKey: string | null = null;
+		try {
+			if (p.instanceId == null) {
+				const deployResponse = await appManager.deploy(req.body.config);
 				await db.project.update({
 					where: {
 						id: projectId,
@@ -218,50 +259,41 @@
 					data: {
 						state,
 						draft: null,
-						instanceId: r.data.id,
-						deployKey: r.data.deployKey,
+						instanceId: deployResponse.id,
+						deployKey: deployResponse.deployKey,
 					},
 				});
-
-				if (p.githubToken && r.data.deployKey) {
-					const stateObj = JSON.parse(JSON.parse(state.toString()));
-					const githubNodes = stateObj.nodes.filter(
-						// eslint-disable-next-line @typescript-eslint/no-explicit-any
-						(n: any) => n.type === "github" && n.data?.repository?.id,
-					);
-
-					const github = new GithubClient(p.githubToken);
-					for (const node of githubNodes) {
-						try {
-							await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
-						} catch (error) {
-							console.error(
-								`Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
-								error,
-							);
-						}
-					}
+				diff = { toAdd: extractGithubRepos(state) };
+				deployKey = deployResponse.deployKey;
+			} else {
+				const success = await appManager.update(p.instanceId, req.body.config);
+				if (success) {
+					diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
+					deployKey = p.deployKey;
+					await db.project.update({
+						where: {
+							id: projectId,
+						},
+						data: {
+							state,
+							draft: null,
+						},
+					});
+				} else {
+					resp.status(500);
+					resp.write(JSON.stringify({ error: "Failed to update deployment" }));
+					return;
 				}
 			}
-		} else {
-			r = await axios.request({
-				url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
-				method: "put",
-				data: {
-					config: req.body.config,
-				},
-			});
-			if (r.status === 200) {
-				await db.project.update({
-					where: {
-						id: projectId,
-					},
-					data: {
-						state,
-						draft: null,
-					},
-				});
+			if (diff && p.githubToken && deployKey) {
+				const github = new GithubClient(p.githubToken);
+				await manageGithubKeys(github, deployKey, diff);
 			}
+			resp.status(200);
+		} catch (error) {
+			console.error("Deployment error:", error);
+			resp.status(500);
+			resp.write(JSON.stringify({ error: "Deployment failed" }));
 		}
 	} catch (e) {
 		console.log(e);
@@ -291,13 +323,13 @@
 			resp.status(404);
 			return;
 		}
-		const r = await axios.request({
-			url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
-			method: "get",
-		});
-		resp.status(r.status);
-		if (r.status === 200) {
-			resp.write(JSON.stringify(r.data));
+		try {
+			const status = await appManager.getStatus(p.instanceId);
+			resp.status(200);
+			resp.write(JSON.stringify(status));
+		} catch (error) {
+			console.error("Error getting status:", error);
+			resp.status(500);
 		}
 	} catch (e) {
 		console.log(e);
@@ -319,16 +351,13 @@
 				githubToken: true,
 			},
 		});
-
 		if (!project?.githubToken) {
 			resp.status(400);
 			resp.write(JSON.stringify({ error: "GitHub token not configured" }));
 			return;
 		}
-
 		const github = new GithubClient(project.githubToken);
 		const repositories = await github.getRepositories();
-
 		resp.status(200);
 		resp.header("Content-Type", "application/json");
 		resp.write(JSON.stringify(repositories));
@@ -345,7 +374,6 @@
 	try {
 		const projectId = Number(req.params["projectId"]);
 		const { githubToken } = req.body;
-
 		await db.project.update({
 			where: {
 				id: projectId,
@@ -353,7 +381,6 @@
 			},
 			data: { githubToken },
 		});
-
 		resp.status(200);
 	} catch (e) {
 		console.log(e);
@@ -376,16 +403,13 @@
 				githubToken: true,
 			},
 		});
-
 		if (!project) {
 			resp.status(404);
 			resp.write(JSON.stringify({ error: "Project not found" }));
 			return;
 		}
-
 		const projectLogs = logs.get(projectId) || new Map();
 		const services = Array.from(projectLogs.keys());
-
 		resp.status(200);
 		resp.write(
 			JSON.stringify({
@@ -406,6 +430,10 @@
 					},
 				],
 				services,
+				user: {
+					id: resp.locals.userId,
+					username: resp.locals.username,
+				},
 			}),
 		);
 	} catch (error) {
@@ -432,21 +460,18 @@
 			resp.write(JSON.stringify({ error: "Project not found" }));
 			return;
 		}
-
 		const projectLogs = logs.get(projectId);
 		if (!projectLogs) {
 			resp.status(404);
 			resp.write(JSON.stringify({ error: "No logs found for this project" }));
 			return;
 		}
-
 		const serviceLog = projectLogs.get(service);
 		if (!serviceLog) {
 			resp.status(404);
 			resp.write(JSON.stringify({ error: "No logs found for this service" }));
 			return;
 		}
-
 		resp.status(200);
 		resp.write(JSON.stringify({ logs: serviceLog }));
 	} catch (e) {
@@ -467,7 +492,6 @@
 const handleRegisterWorker: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
-
 		const result = WorkerSchema.safeParse(req.body);
 		if (!result.success) {
 			resp.status(400);
@@ -479,17 +503,11 @@
 			);
 			return;
 		}
-
 		const { service, address, logs: log } = result.data;
-
-		// Get existing workers or initialize empty array
 		const projectWorkers = workers.get(projectId) || [];
-
-		// Add new worker if not already present
 		if (!projectWorkers.includes(address)) {
 			projectWorkers.push(address);
 		}
-
 		workers.set(projectId, projectWorkers);
 		if (log) {
 			const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
@@ -526,13 +544,11 @@
 			resp.write(JSON.stringify({ error: "Project not found" }));
 			return;
 		}
-
 		if (projectWorkers.length === 0) {
 			resp.status(404);
 			resp.write(JSON.stringify({ error: "No workers registered for this project" }));
 			return;
 		}
-
 		await Promise.all(
 			projectWorkers.map(async (workerAddress) => {
 				try {
@@ -544,7 +560,6 @@
 				}
 			}),
 		);
-
 		resp.status(200);
 		resp.write(JSON.stringify({ success: true }));
 	} catch (e) {
@@ -558,13 +573,15 @@
 
 const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
 	const userId = req.get("x-forwarded-userid");
-	if (userId === undefined) {
+	const username = req.get("x-forwarded-user");
+	if (userId == null || username == null) {
 		resp.status(401);
 		resp.write("Unauthorized");
 		resp.end();
 		return;
 	}
 	resp.locals.userId = userId;
+	resp.locals.username = username;
 	next();
 };
 
@@ -592,7 +609,6 @@
 	api.use(express.json());
 	api.post("/api/project/:projectId/workers", handleRegisterWorker);
 
-	// Start both servers
 	app.listen(env.DODO_PORT_WEB, () => {
 		console.log("Web server started on port", env.DODO_PORT_WEB);
 	});