blob: 3c276d7f4510048ea0e14c47b204f2a02af472c2 [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({
26 id,
27 }),
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 }
151 const r = await axios.request({
152 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
153 method: "post",
154 });
155 if (r.status === 200) {
156 await db.project.delete({
157 where: {
158 id: projectId,
159 },
160 });
161 }
162 resp.status(200);
163 } catch (e) {
164 console.log(e);
165 resp.status(500);
166 } finally {
167 resp.end();
168 }
169};
170
171const handleDeploy: express.Handler = async (req, resp) => {
172 try {
173 const projectId = Number(req.params["projectId"]);
174 const state = Buffer.from(JSON.stringify(req.body.state));
175 const p = await db.project.findUnique({
176 where: {
177 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000178 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000179 },
180 select: {
181 instanceId: true,
182 githubToken: true,
183 deployKey: true,
184 },
185 });
186 if (p === null) {
187 resp.status(404);
188 return;
189 }
190 await db.project.update({
191 where: {
192 id: projectId,
193 },
194 data: {
195 draft: state,
196 },
197 });
198 let r: { status: number; data: { id: string; deployKey: string } };
199 if (p.instanceId == null) {
200 r = await axios.request({
201 url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
202 method: "post",
203 data: {
204 config: req.body.config,
205 },
206 });
giod0026612025-05-08 13:00:36 +0000207 if (r.status === 200) {
208 await db.project.update({
209 where: {
210 id: projectId,
211 },
212 data: {
213 state,
214 draft: null,
215 instanceId: r.data.id,
216 deployKey: r.data.deployKey,
217 },
218 });
219
220 if (p.githubToken && r.data.deployKey) {
221 const stateObj = JSON.parse(JSON.parse(state.toString()));
gio0b4002c2025-05-11 15:48:51 +0000222 const githubNodes = stateObj.nodes.filter(
223 // eslint-disable-next-line @typescript-eslint/no-explicit-any
224 (n: any) => n.type === "github" && n.data?.repository?.id,
225 );
giod0026612025-05-08 13:00:36 +0000226
227 const github = new GithubClient(p.githubToken);
228 for (const node of githubNodes) {
229 try {
230 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
231 } catch (error) {
232 console.error(
233 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
234 error,
235 );
236 }
237 }
238 }
239 }
240 } else {
241 r = await axios.request({
242 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
243 method: "put",
244 data: {
245 config: req.body.config,
246 },
247 });
248 if (r.status === 200) {
249 await db.project.update({
250 where: {
251 id: projectId,
252 },
253 data: {
254 state,
255 draft: null,
256 },
257 });
258 }
259 }
260 } catch (e) {
261 console.log(e);
262 resp.status(500);
263 } finally {
264 resp.end();
265 }
266};
267
268const handleStatus: express.Handler = async (req, resp) => {
269 try {
270 const projectId = Number(req.params["projectId"]);
271 const p = await db.project.findUnique({
272 where: {
273 id: projectId,
gio09fcab52025-05-12 14:05:07 +0000274 userId: resp.locals.userId,
giod0026612025-05-08 13:00:36 +0000275 },
276 select: {
277 instanceId: true,
278 },
279 });
giod0026612025-05-08 13:00:36 +0000280 if (p === null) {
281 resp.status(404);
282 return;
283 }
284 if (p.instanceId == null) {
285 resp.status(404);
286 return;
287 }
288 const r = await axios.request({
289 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
290 method: "get",
291 });
292 resp.status(r.status);
293 if (r.status === 200) {
294 resp.write(JSON.stringify(r.data));
295 }
296 } catch (e) {
297 console.log(e);
298 resp.status(500);
299 } finally {
300 resp.end();
301 }
302};
303
304const handleGithubRepos: express.Handler = async (req, resp) => {
305 try {
306 const projectId = Number(req.params["projectId"]);
307 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000308 where: {
309 id: projectId,
310 userId: resp.locals.userId,
311 },
312 select: {
313 githubToken: true,
314 },
giod0026612025-05-08 13:00:36 +0000315 });
316
317 if (!project?.githubToken) {
318 resp.status(400);
319 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
320 return;
321 }
322
323 const github = new GithubClient(project.githubToken);
324 const repositories = await github.getRepositories();
325
326 resp.status(200);
327 resp.header("Content-Type", "application/json");
328 resp.write(JSON.stringify(repositories));
329 } catch (e) {
330 console.log(e);
331 resp.status(500);
332 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
333 } finally {
334 resp.end();
335 }
336};
337
338const handleUpdateGithubToken: express.Handler = async (req, resp) => {
339 try {
340 const projectId = Number(req.params["projectId"]);
341 const { githubToken } = req.body;
342
343 await db.project.update({
gio09fcab52025-05-12 14:05:07 +0000344 where: {
345 id: projectId,
346 userId: resp.locals.userId,
347 },
giod0026612025-05-08 13:00:36 +0000348 data: { githubToken },
349 });
350
351 resp.status(200);
352 } catch (e) {
353 console.log(e);
354 resp.status(500);
355 } finally {
356 resp.end();
357 }
358};
359
360const handleEnv: express.Handler = async (req, resp) => {
361 const projectId = Number(req.params["projectId"]);
362 try {
363 const project = await db.project.findUnique({
gio09fcab52025-05-12 14:05:07 +0000364 where: {
365 id: projectId,
366 userId: resp.locals.userId,
367 },
giod0026612025-05-08 13:00:36 +0000368 select: {
369 deployKey: true,
370 githubToken: true,
371 },
372 });
373
374 if (!project) {
375 resp.status(404);
376 resp.write(JSON.stringify({ error: "Project not found" }));
377 return;
378 }
379
gio3a921b82025-05-10 07:36:09 +0000380 const projectLogs = logs.get(projectId) || new Map();
381 const services = Array.from(projectLogs.keys());
382
giod0026612025-05-08 13:00:36 +0000383 resp.status(200);
384 resp.write(
385 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000386 // TODO(gio): get from env or command line flags
gio09fcab52025-05-12 14:05:07 +0000387 managerAddr: "http://10.42.0.211:8081",
giod0026612025-05-08 13:00:36 +0000388 deployKey: project.deployKey,
389 integrations: {
390 github: !!project.githubToken,
391 },
392 networks: [
393 {
394 name: "Public",
395 domain: "v1.dodo.cloud",
396 },
397 {
398 name: "Private",
399 domain: "p.v1.dodo.cloud",
400 },
401 ],
gio3a921b82025-05-10 07:36:09 +0000402 services,
giod0026612025-05-08 13:00:36 +0000403 }),
404 );
405 } catch (error) {
406 console.error("Error checking integrations:", error);
407 resp.status(500);
408 resp.write(JSON.stringify({ error: "Internal server error" }));
409 } finally {
410 resp.end();
411 }
412};
413
gio3a921b82025-05-10 07:36:09 +0000414const handleServiceLogs: express.Handler = async (req, resp) => {
415 try {
416 const projectId = Number(req.params["projectId"]);
417 const service = req.params["service"];
gio09fcab52025-05-12 14:05:07 +0000418 const project = await db.project.findUnique({
419 where: {
420 id: projectId,
421 userId: resp.locals.userId,
422 },
423 });
424 if (project == null) {
425 resp.status(404);
426 resp.write(JSON.stringify({ error: "Project not found" }));
427 return;
428 }
gio3a921b82025-05-10 07:36:09 +0000429
430 const projectLogs = logs.get(projectId);
431 if (!projectLogs) {
432 resp.status(404);
433 resp.write(JSON.stringify({ error: "No logs found for this project" }));
434 return;
435 }
436
437 const serviceLog = projectLogs.get(service);
438 if (!serviceLog) {
439 resp.status(404);
440 resp.write(JSON.stringify({ error: "No logs found for this service" }));
441 return;
442 }
443
444 resp.status(200);
445 resp.write(JSON.stringify({ logs: serviceLog }));
446 } catch (e) {
447 console.log(e);
448 resp.status(500);
449 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
450 } finally {
451 resp.end();
452 }
453};
454
gio7d813702025-05-08 18:29:52 +0000455const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000456 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000457 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000458 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000459});
460
461const handleRegisterWorker: express.Handler = async (req, resp) => {
462 try {
463 const projectId = Number(req.params["projectId"]);
464
465 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000466 if (!result.success) {
467 resp.status(400);
468 resp.write(
469 JSON.stringify({
470 error: "Invalid request data",
471 details: result.error.format(),
472 }),
473 );
474 return;
475 }
476
gio3a921b82025-05-10 07:36:09 +0000477 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000478
479 // Get existing workers or initialize empty array
480 const projectWorkers = workers.get(projectId) || [];
481
482 // Add new worker if not already present
483 if (!projectWorkers.includes(address)) {
484 projectWorkers.push(address);
485 }
486
487 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000488 if (log) {
489 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
490 svcLogs.set(service, log);
491 logs.set(projectId, svcLogs);
492 }
gio7d813702025-05-08 18:29:52 +0000493 resp.status(200);
494 resp.write(
495 JSON.stringify({
496 success: true,
gio7d813702025-05-08 18:29:52 +0000497 }),
498 );
499 } catch (e) {
500 console.log(e);
501 resp.status(500);
502 resp.write(JSON.stringify({ error: "Failed to register worker" }));
503 } finally {
504 resp.end();
505 }
506};
507
508const handleReload: express.Handler = async (req, resp) => {
509 try {
510 const projectId = Number(req.params["projectId"]);
511 const projectWorkers = workers.get(projectId) || [];
gio09fcab52025-05-12 14:05:07 +0000512 const project = await db.project.findUnique({
513 where: {
514 id: projectId,
515 userId: resp.locals.userId,
516 },
517 });
518 if (project == null) {
519 resp.status(404);
520 resp.write(JSON.stringify({ error: "Project not found" }));
521 return;
522 }
gio7d813702025-05-08 18:29:52 +0000523
524 if (projectWorkers.length === 0) {
525 resp.status(404);
526 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
527 return;
528 }
529
530 await Promise.all(
531 projectWorkers.map(async (workerAddress) => {
532 try {
533 const updateEndpoint = `${workerAddress}/update`;
534 await axios.post(updateEndpoint);
gio0b4002c2025-05-11 15:48:51 +0000535 // eslint-disable-next-line @typescript-eslint/no-explicit-any
536 } catch (error: any) {
gio7d813702025-05-08 18:29:52 +0000537 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
538 }
539 }),
540 );
541
542 resp.status(200);
543 resp.write(JSON.stringify({ success: true }));
544 } catch (e) {
545 console.log(e);
546 resp.status(500);
547 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
548 } finally {
549 resp.end();
550 }
551};
552
gio09fcab52025-05-12 14:05:07 +0000553const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
554 const userId = req.get("x-forwarded-userid");
555 if (userId === undefined) {
556 resp.status(401);
557 resp.write("Unauthorized");
558 resp.end();
559 return;
560 }
561 resp.locals.userId = userId;
562 next();
563};
564
giod0026612025-05-08 13:00:36 +0000565async function start() {
566 await db.$connect();
567 const app = express();
568 app.use(express.json());
gio09fcab52025-05-12 14:05:07 +0000569 app.use(auth);
giod0026612025-05-08 13:00:36 +0000570 app.post("/api/project/:projectId/saved", handleSave);
gio818da4e2025-05-12 14:45:35 +0000571 app.get("/api/project/:projectId/saved/deploy", handleSavedGet("deploy"));
572 app.get("/api/project/:projectId/saved/draft", handleSavedGet("draft"));
giod0026612025-05-08 13:00:36 +0000573 app.post("/api/project/:projectId/deploy", handleDeploy);
574 app.get("/api/project/:projectId/status", handleStatus);
575 app.delete("/api/project/:projectId", handleDelete);
576 app.get("/api/project", handleProjectAll);
577 app.post("/api/project", handleProjectCreate);
578 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
579 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
580 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000581 app.post("/api/project/:projectId/reload", handleReload);
gio3a921b82025-05-10 07:36:09 +0000582 app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
giod0026612025-05-08 13:00:36 +0000583 app.use("/", express.static("../front/dist"));
gio09fcab52025-05-12 14:05:07 +0000584
585 const api = express();
586 api.use(express.json());
587 api.post("/api/project/:projectId/workers", handleRegisterWorker);
588
589 // Start both servers
giod0026612025-05-08 13:00:36 +0000590 app.listen(env.DODO_PORT_WEB, () => {
gio09fcab52025-05-12 14:05:07 +0000591 console.log("Web server started on port", env.DODO_PORT_WEB);
592 });
593
594 api.listen(env.DODO_PORT_API, () => {
595 console.log("Internal API server started on port", env.DODO_PORT_API);
giod0026612025-05-08 13:00:36 +0000596 });
597}
598
599start();