blob: 295bdf361d246f88670da74fe3829facadec819c [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import { PrismaClient } from "@prisma/client";
2import express from "express";
3import { env } from "node:process";
4import axios from "axios";
5import { GithubClient } from "./github";
gio7d813702025-05-08 18:29:52 +00006import { z } from "zod";
giod0026612025-05-08 13:00:36 +00007
8const db = new PrismaClient();
9
gio7d813702025-05-08 18:29:52 +000010// Map to store worker addresses by project ID
11const workers = new Map<number, string[]>();
gio3a921b82025-05-10 07:36:09 +000012const logs = new Map<number, Map<string, string>>();
gio7d813702025-05-08 18:29:52 +000013
giod0026612025-05-08 13:00:36 +000014const handleProjectCreate: express.Handler = async (req, resp) => {
15 try {
16 const { id } = await db.project.create({
17 data: {
gio09fcab52025-05-12 14:05:07 +000018 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000019 name: req.body.name,
20 },
21 });
22 resp.status(200);
23 resp.header("Content-Type", "application/json");
24 resp.write(
25 JSON.stringify({
gio74ab7852025-05-13 13:19:31 +000026 id: id.toString(),
giod0026612025-05-08 13:00:36 +000027 }),
28 );
29 } catch (e) {
30 console.log(e);
31 resp.status(500);
32 } finally {
33 resp.end();
34 }
35};
36
37const handleProjectAll: express.Handler = async (req, resp) => {
38 try {
39 const r = await db.project.findMany({
40 where: {
gio09fcab52025-05-12 14:05:07 +000041 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000042 },
43 });
44 resp.status(200);
45 resp.header("Content-Type", "application/json");
46 resp.write(
47 JSON.stringify(
48 r.map((p) => ({
49 id: p.id.toString(),
50 name: p.name,
51 })),
52 ),
53 );
54 } catch (e) {
55 console.log(e);
56 resp.status(500);
57 } finally {
58 resp.end();
59 }
60};
61
62const handleSave: express.Handler = async (req, resp) => {
63 try {
64 await db.project.update({
65 where: {
66 id: Number(req.params["projectId"]),
gio09fcab52025-05-12 14:05:07 +000067 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +000068 },
69 data: {
70 draft: Buffer.from(JSON.stringify(req.body)),
71 },
72 });
73 resp.status(200);
74 } catch (e) {
75 console.log(e);
76 resp.status(500);
77 } finally {
78 resp.end();
79 }
80};
81
gio818da4e2025-05-12 14:45:35 +000082function handleSavedGet(state: "deploy" | "draft"): express.Handler {
83 return async (req, resp) => {
84 try {
85 const r = await db.project.findUnique({
86 where: {
87 id: Number(req.params["projectId"]),
88 userId: resp.locals.userId,
89 },
90 select: {
91 state: true,
92 draft: true,
93 },
94 });
95 if (r == null) {
96 resp.status(404);
97 return;
98 }
giod0026612025-05-08 13:00:36 +000099 resp.status(200);
100 resp.header("content-type", "application/json");
gio818da4e2025-05-12 14:45:35 +0000101 if (state === "deploy") {
giod0026612025-05-08 13:00:36 +0000102 if (r.state == null) {
103 resp.send({
104 nodes: [],
105 edges: [],
106 viewport: { x: 0, y: 0, zoom: 1 },
107 });
108 } else {
109 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
110 }
111 } else {
gio818da4e2025-05-12 14:45:35 +0000112 if (r.draft == null) {
113 if (r.state == null) {
114 resp.send({
115 nodes: [],
116 edges: [],
117 viewport: { x: 0, y: 0, zoom: 1 },
118 });
119 } else {
120 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
121 }
122 } else {
123 resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
124 }
giod0026612025-05-08 13:00:36 +0000125 }
gio818da4e2025-05-12 14:45:35 +0000126 } catch (e) {
127 console.log(e);
128 resp.status(500);
129 } finally {
130 resp.end();
giod0026612025-05-08 13:00:36 +0000131 }
gio818da4e2025-05-12 14:45:35 +0000132 };
133}
giod0026612025-05-08 13:00:36 +0000134
135const handleDelete: express.Handler = async (req, resp) => {
136 try {
137 const projectId = Number(req.params["projectId"]);
138 const p = await db.project.findUnique({
139 where: {
140 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000141 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000142 },
143 select: {
144 instanceId: true,
145 },
146 });
147 if (p === null) {
148 resp.status(404);
149 return;
150 }
gioe440db82025-05-13 12:21:44 +0000151 let ok = false;
152 if (p.instanceId === null) {
153 ok = true;
154 } else {
155 const r = await axios.request({
156 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
157 method: "post",
158 });
159 ok = r.status === 200;
160 }
161 if (ok) {
giod0026612025-05-08 13:00:36 +0000162 await db.project.delete({
163 where: {
164 id: projectId,
165 },
166 });
167 }
168 resp.status(200);
169 } catch (e) {
170 console.log(e);
171 resp.status(500);
172 } finally {
173 resp.end();
174 }
175};
176
177const handleDeploy: express.Handler = async (req, resp) => {
178 try {
179 const projectId = Number(req.params["projectId"]);
180 const state = Buffer.from(JSON.stringify(req.body.state));
181 const p = await db.project.findUnique({
182 where: {
183 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000184 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000185 },
186 select: {
187 instanceId: true,
188 githubToken: true,
189 deployKey: true,
190 },
191 });
192 if (p === null) {
193 resp.status(404);
194 return;
195 }
196 await db.project.update({
197 where: {
198 id: projectId,
199 },
200 data: {
201 draft: state,
202 },
203 });
204 let r: { status: number; data: { id: string; deployKey: string } };
205 if (p.instanceId == null) {
206 r = await axios.request({
207 url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
208 method: "post",
209 data: {
210 config: req.body.config,
211 },
212 });
giod0026612025-05-08 13:00:36 +0000213 if (r.status === 200) {
214 await db.project.update({
215 where: {
216 id: projectId,
217 },
218 data: {
219 state,
220 draft: null,
221 instanceId: r.data.id,
222 deployKey: r.data.deployKey,
223 },
224 });
225
226 if (p.githubToken && r.data.deployKey) {
227 const stateObj = JSON.parse(JSON.parse(state.toString()));
gio0b4002c2025-05-11 15:48:51 +0000228 const githubNodes = stateObj.nodes.filter(
229 // eslint-disable-next-line @typescript-eslint/no-explicit-any
230 (n: any) => n.type === "github" && n.data?.repository?.id,
231 );
giod0026612025-05-08 13:00:36 +0000232
233 const github = new GithubClient(p.githubToken);
234 for (const node of githubNodes) {
235 try {
236 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
237 } catch (error) {
238 console.error(
239 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
240 error,
241 );
242 }
243 }
244 }
245 }
246 } else {
247 r = await axios.request({
248 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
249 method: "put",
250 data: {
251 config: req.body.config,
252 },
253 });
254 if (r.status === 200) {
255 await db.project.update({
256 where: {
257 id: projectId,
258 },
259 data: {
260 state,
261 draft: null,
262 },
263 });
264 }
265 }
266 } catch (e) {
267 console.log(e);
268 resp.status(500);
269 } finally {
270 resp.end();
271 }
272};
273
274const handleStatus: express.Handler = async (req, resp) => {
275 try {
276 const projectId = Number(req.params["projectId"]);
277 const p = await db.project.findUnique({
278 where: {
279 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000280 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000281 },
282 select: {
283 instanceId: true,
284 },
285 });
giod0026612025-05-08 13:00:36 +0000286 if (p === null) {
287 resp.status(404);
288 return;
289 }
290 if (p.instanceId == null) {
291 resp.status(404);
292 return;
293 }
294 const r = await axios.request({
295 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
296 method: "get",
297 });
298 resp.status(r.status);
299 if (r.status === 200) {
300 resp.write(JSON.stringify(r.data));
301 }
302 } catch (e) {
303 console.log(e);
304 resp.status(500);
305 } finally {
306 resp.end();
307 }
308};
309
310const handleGithubRepos: express.Handler = async (req, resp) => {
311 try {
312 const projectId = Number(req.params["projectId"]);
313 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000314 where: {
315 id: projectId,
316 userId: resp.locals.userId,
317 },
318 select: {
319 githubToken: true,
320 },
giod0026612025-05-08 13:00:36 +0000321 });
322
323 if (!project?.githubToken) {
324 resp.status(400);
325 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
326 return;
327 }
328
329 const github = new GithubClient(project.githubToken);
330 const repositories = await github.getRepositories();
331
332 resp.status(200);
333 resp.header("Content-Type", "application/json");
334 resp.write(JSON.stringify(repositories));
335 } catch (e) {
336 console.log(e);
337 resp.status(500);
338 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
339 } finally {
340 resp.end();
341 }
342};
343
344const handleUpdateGithubToken: express.Handler = async (req, resp) => {
345 try {
346 const projectId = Number(req.params["projectId"]);
347 const { githubToken } = req.body;
348
349 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000350 where: {
351 id: projectId,
352 userId: resp.locals.userId,
353 },
giod0026612025-05-08 13:00:36 +0000354 data: { githubToken },
355 });
356
357 resp.status(200);
358 } catch (e) {
359 console.log(e);
360 resp.status(500);
361 } finally {
362 resp.end();
363 }
364};
365
366const handleEnv: express.Handler = async (req, resp) => {
367 const projectId = Number(req.params["projectId"]);
368 try {
369 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000370 where: {
371 id: projectId,
372 userId: resp.locals.userId,
373 },
giod0026612025-05-08 13:00:36 +0000374 select: {
375 deployKey: true,
376 githubToken: true,
377 },
378 });
379
380 if (!project) {
381 resp.status(404);
382 resp.write(JSON.stringify({ error: "Project not found" }));
383 return;
384 }
385
gio3a921b82025-05-10 07:36:09 +0000386 const projectLogs = logs.get(projectId) || new Map();
387 const services = Array.from(projectLogs.keys());
388
giod0026612025-05-08 13:00:36 +0000389 resp.status(200);
390 resp.write(
391 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000392 // TODO(gio): get from env or command line flags
gio09fcab52025-05-12 14:05:07 +0000393 managerAddr: "http://10.42.0.211:8081",
giod0026612025-05-08 13:00:36 +0000394 deployKey: project.deployKey,
395 integrations: {
396 github: !!project.githubToken,
397 },
398 networks: [
399 {
400 name: "Public",
401 domain: "v1.dodo.cloud",
402 },
403 {
404 name: "Private",
405 domain: "p.v1.dodo.cloud",
406 },
407 ],
gio3a921b82025-05-10 07:36:09 +0000408 services,
giod0026612025-05-08 13:00:36 +0000409 }),
410 );
411 } catch (error) {
412 console.error("Error checking integrations:", error);
413 resp.status(500);
414 resp.write(JSON.stringify({ error: "Internal server error" }));
415 } finally {
416 resp.end();
417 }
418};
419
gio3a921b82025-05-10 07:36:09 +0000420const handleServiceLogs: express.Handler = async (req, resp) => {
421 try {
422 const projectId = Number(req.params["projectId"]);
423 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000424 const project = await db.project.findUnique({
425 where: {
426 id: projectId,
427 userId: resp.locals.userId,
428 },
429 });
430 if (project == null) {
431 resp.status(404);
432 resp.write(JSON.stringify({ error: "Project not found" }));
433 return;
434 }
gio3a921b82025-05-10 07:36:09 +0000435
436 const projectLogs = logs.get(projectId);
437 if (!projectLogs) {
438 resp.status(404);
439 resp.write(JSON.stringify({ error: "No logs found for this project" }));
440 return;
441 }
442
443 const serviceLog = projectLogs.get(service);
444 if (!serviceLog) {
445 resp.status(404);
446 resp.write(JSON.stringify({ error: "No logs found for this service" }));
447 return;
448 }
449
450 resp.status(200);
451 resp.write(JSON.stringify({ logs: serviceLog }));
452 } catch (e) {
453 console.log(e);
454 resp.status(500);
455 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
456 } finally {
457 resp.end();
458 }
459};
460
gio7d813702025-05-08 18:29:52 +0000461const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000462 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000463 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000464 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000465});
466
467const handleRegisterWorker: express.Handler = async (req, resp) => {
468 try {
469 const projectId = Number(req.params["projectId"]);
470
471 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000472 if (!result.success) {
473 resp.status(400);
474 resp.write(
475 JSON.stringify({
476 error: "Invalid request data",
477 details: result.error.format(),
478 }),
479 );
480 return;
481 }
482
gio3a921b82025-05-10 07:36:09 +0000483 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000484
485 // Get existing workers or initialize empty array
486 const projectWorkers = workers.get(projectId) || [];
487
488 // Add new worker if not already present
489 if (!projectWorkers.includes(address)) {
490 projectWorkers.push(address);
491 }
492
493 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000494 if (log) {
495 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
496 svcLogs.set(service, log);
497 logs.set(projectId, svcLogs);
498 }
gio7d813702025-05-08 18:29:52 +0000499 resp.status(200);
500 resp.write(
501 JSON.stringify({
502 success: true,
gio7d813702025-05-08 18:29:52 +0000503 }),
504 );
505 } catch (e) {
506 console.log(e);
507 resp.status(500);
508 resp.write(JSON.stringify({ error: "Failed to register worker" }));
509 } finally {
510 resp.end();
511 }
512};
513
514const handleReload: express.Handler = async (req, resp) => {
515 try {
516 const projectId = Number(req.params["projectId"]);
517 const projectWorkers = workers.get(projectId) || [];
gio09fcab52025-05-12 14:05:07 +0000518 const project = await db.project.findUnique({
519 where: {
520 id: projectId,
521 userId: resp.locals.userId,
522 },
523 });
524 if (project == null) {
525 resp.status(404);
526 resp.write(JSON.stringify({ error: "Project not found" }));
527 return;
528 }
gio7d813702025-05-08 18:29:52 +0000529
530 if (projectWorkers.length === 0) {
531 resp.status(404);
532 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
533 return;
534 }
535
536 await Promise.all(
537 projectWorkers.map(async (workerAddress) => {
538 try {
539 const updateEndpoint = `${workerAddress}/update`;
540 await axios.post(updateEndpoint);
gio0b4002c2025-05-11 15:48:51 +0000541 // eslint-disable-next-line @typescript-eslint/no-explicit-any
542 } catch (error: any) {
gio7d813702025-05-08 18:29:52 +0000543 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
544 }
545 }),
546 );
547
548 resp.status(200);
549 resp.write(JSON.stringify({ success: true }));
550 } catch (e) {
551 console.log(e);
552 resp.status(500);
553 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
554 } finally {
555 resp.end();
556 }
557};
558
gio09fcab52025-05-12 14:05:07 +0000559const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
560 const userId = req.get("x-forwarded-userid");
561 if (userId === undefined) {
562 resp.status(401);
563 resp.write("Unauthorized");
564 resp.end();
565 return;
566 }
567 resp.locals.userId = userId;
568 next();
569};
570
giod0026612025-05-08 13:00:36 +0000571async function start() {
572 await db.$connect();
573 const app = express();
574 app.use(express.json());
gio09fcab52025-05-12 14:05:07 +0000575 app.use(auth);
giod0026612025-05-08 13:00:36 +0000576 app.post("/api/project/:projectId/saved", handleSave);
gio818da4e2025-05-12 14:45:35 +0000577 app.get("/api/project/:projectId/saved/deploy", handleSavedGet("deploy"));
578 app.get("/api/project/:projectId/saved/draft", handleSavedGet("draft"));
giod0026612025-05-08 13:00:36 +0000579 app.post("/api/project/:projectId/deploy", handleDeploy);
580 app.get("/api/project/:projectId/status", handleStatus);
581 app.delete("/api/project/:projectId", handleDelete);
582 app.get("/api/project", handleProjectAll);
583 app.post("/api/project", handleProjectCreate);
584 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
585 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
586 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000587 app.post("/api/project/:projectId/reload", handleReload);
gio3a921b82025-05-10 07:36:09 +0000588 app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
giod0026612025-05-08 13:00:36 +0000589 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000590
591 const api = express();
592 api.use(express.json());
593 api.post("/api/project/:projectId/workers", handleRegisterWorker);
594
595 // Start both servers
giod0026612025-05-08 13:00:36 +0000596 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000597 console.log("Web server started on port", env.DODO_PORT_WEB);
598 });
599
600 api.listen(env.DODO_PORT_API, () => {
601 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000602 });
603}
604
605start();