blob: d5c1b2d134fb89b1e11d961fcd3ecc0ad9475396 [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: {
18 userId: "gio", // req.get("x-forwarded-userid")!,
19 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: {
41 userId: "gio", // req.get("x-forwarded-userid")!,
42 },
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"]),
67 },
68 data: {
69 draft: Buffer.from(JSON.stringify(req.body)),
70 },
71 });
72 resp.status(200);
73 } catch (e) {
74 console.log(e);
75 resp.status(500);
76 } finally {
77 resp.end();
78 }
79};
80
81const handleSavedGet: express.Handler = async (req, resp) => {
82 try {
83 const r = await db.project.findUnique({
84 where: {
85 id: Number(req.params["projectId"]),
86 },
87 select: {
88 state: true,
89 draft: true,
90 },
91 });
92 if (r == null) {
93 resp.status(404);
94 } else {
95 resp.status(200);
96 resp.header("content-type", "application/json");
97 if (r.draft == null) {
98 if (r.state == null) {
99 resp.send({
100 nodes: [],
101 edges: [],
102 viewport: { x: 0, y: 0, zoom: 1 },
103 });
104 } else {
105 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
106 }
107 } else {
108 resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
109 }
110 }
111 } catch (e) {
112 console.log(e);
113 resp.status(500);
114 } finally {
115 resp.end();
116 }
117};
118
119const handleDelete: express.Handler = async (req, resp) => {
120 try {
121 const projectId = Number(req.params["projectId"]);
122 const p = await db.project.findUnique({
123 where: {
124 id: projectId,
125 },
126 select: {
127 instanceId: true,
128 },
129 });
130 if (p === null) {
131 resp.status(404);
132 return;
133 }
134 const r = await axios.request({
135 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
136 method: "post",
137 });
138 if (r.status === 200) {
139 await db.project.delete({
140 where: {
141 id: projectId,
142 },
143 });
144 }
145 resp.status(200);
146 } catch (e) {
147 console.log(e);
148 resp.status(500);
149 } finally {
150 resp.end();
151 }
152};
153
154const handleDeploy: express.Handler = async (req, resp) => {
155 try {
156 const projectId = Number(req.params["projectId"]);
157 const state = Buffer.from(JSON.stringify(req.body.state));
158 const p = await db.project.findUnique({
159 where: {
160 id: projectId,
161 },
162 select: {
163 instanceId: true,
164 githubToken: true,
165 deployKey: true,
166 },
167 });
168 if (p === null) {
169 resp.status(404);
170 return;
171 }
172 await db.project.update({
173 where: {
174 id: projectId,
175 },
176 data: {
177 draft: state,
178 },
179 });
180 let r: { status: number; data: { id: string; deployKey: string } };
181 if (p.instanceId == null) {
182 r = await axios.request({
183 url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
184 method: "post",
185 data: {
186 config: req.body.config,
187 },
188 });
189 console.log(r);
190 if (r.status === 200) {
191 await db.project.update({
192 where: {
193 id: projectId,
194 },
195 data: {
196 state,
197 draft: null,
198 instanceId: r.data.id,
199 deployKey: r.data.deployKey,
200 },
201 });
202
203 if (p.githubToken && r.data.deployKey) {
204 const stateObj = JSON.parse(JSON.parse(state.toString()));
gio0b4002c2025-05-11 15:48:51 +0000205 const githubNodes = stateObj.nodes.filter(
206 // eslint-disable-next-line @typescript-eslint/no-explicit-any
207 (n: any) => n.type === "github" && n.data?.repository?.id,
208 );
giod0026612025-05-08 13:00:36 +0000209
210 const github = new GithubClient(p.githubToken);
211 for (const node of githubNodes) {
212 try {
213 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
214 } catch (error) {
215 console.error(
216 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
217 error,
218 );
219 }
220 }
221 }
222 }
223 } else {
224 r = await axios.request({
225 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
226 method: "put",
227 data: {
228 config: req.body.config,
229 },
230 });
231 if (r.status === 200) {
232 await db.project.update({
233 where: {
234 id: projectId,
235 },
236 data: {
237 state,
238 draft: null,
239 },
240 });
241 }
242 }
243 } catch (e) {
244 console.log(e);
245 resp.status(500);
246 } finally {
247 resp.end();
248 }
249};
250
251const handleStatus: express.Handler = async (req, resp) => {
252 try {
253 const projectId = Number(req.params["projectId"]);
254 const p = await db.project.findUnique({
255 where: {
256 id: projectId,
257 },
258 select: {
259 instanceId: true,
260 },
261 });
262 console.log(projectId, p);
263 if (p === null) {
264 resp.status(404);
265 return;
266 }
267 if (p.instanceId == null) {
268 resp.status(404);
269 return;
270 }
271 const r = await axios.request({
272 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
273 method: "get",
274 });
275 resp.status(r.status);
276 if (r.status === 200) {
277 resp.write(JSON.stringify(r.data));
278 }
279 } catch (e) {
280 console.log(e);
281 resp.status(500);
282 } finally {
283 resp.end();
284 }
285};
286
287const handleGithubRepos: express.Handler = async (req, resp) => {
288 try {
289 const projectId = Number(req.params["projectId"]);
290 const project = await db.project.findUnique({
291 where: { id: projectId },
292 select: { githubToken: true },
293 });
294
295 if (!project?.githubToken) {
296 resp.status(400);
297 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
298 return;
299 }
300
301 const github = new GithubClient(project.githubToken);
302 const repositories = await github.getRepositories();
303
304 resp.status(200);
305 resp.header("Content-Type", "application/json");
306 resp.write(JSON.stringify(repositories));
307 } catch (e) {
308 console.log(e);
309 resp.status(500);
310 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
311 } finally {
312 resp.end();
313 }
314};
315
316const handleUpdateGithubToken: express.Handler = async (req, resp) => {
317 try {
318 const projectId = Number(req.params["projectId"]);
319 const { githubToken } = req.body;
320
321 await db.project.update({
322 where: { id: projectId },
323 data: { githubToken },
324 });
325
326 resp.status(200);
327 } catch (e) {
328 console.log(e);
329 resp.status(500);
330 } finally {
331 resp.end();
332 }
333};
334
335const handleEnv: express.Handler = async (req, resp) => {
336 const projectId = Number(req.params["projectId"]);
337 try {
338 const project = await db.project.findUnique({
339 where: { id: projectId },
340 select: {
341 deployKey: true,
342 githubToken: true,
343 },
344 });
345
346 if (!project) {
347 resp.status(404);
348 resp.write(JSON.stringify({ error: "Project not found" }));
349 return;
350 }
351
gio3a921b82025-05-10 07:36:09 +0000352 const projectLogs = logs.get(projectId) || new Map();
353 const services = Array.from(projectLogs.keys());
354
giod0026612025-05-08 13:00:36 +0000355 resp.status(200);
356 resp.write(
357 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000358 // TODO(gio): get from env or command line flags
359 managerAddr: "http://10.42.0.239:8080",
giod0026612025-05-08 13:00:36 +0000360 deployKey: project.deployKey,
361 integrations: {
362 github: !!project.githubToken,
363 },
364 networks: [
365 {
366 name: "Public",
367 domain: "v1.dodo.cloud",
368 },
369 {
370 name: "Private",
371 domain: "p.v1.dodo.cloud",
372 },
373 ],
gio3a921b82025-05-10 07:36:09 +0000374 services,
giod0026612025-05-08 13:00:36 +0000375 }),
376 );
377 } catch (error) {
378 console.error("Error checking integrations:", error);
379 resp.status(500);
380 resp.write(JSON.stringify({ error: "Internal server error" }));
381 } finally {
382 resp.end();
383 }
384};
385
gio3a921b82025-05-10 07:36:09 +0000386const handleServiceLogs: express.Handler = async (req, resp) => {
387 try {
388 const projectId = Number(req.params["projectId"]);
389 const service = req.params["service"];
390
391 const projectLogs = logs.get(projectId);
392 if (!projectLogs) {
393 resp.status(404);
394 resp.write(JSON.stringify({ error: "No logs found for this project" }));
395 return;
396 }
397
398 const serviceLog = projectLogs.get(service);
399 if (!serviceLog) {
400 resp.status(404);
401 resp.write(JSON.stringify({ error: "No logs found for this service" }));
402 return;
403 }
404
405 resp.status(200);
406 resp.write(JSON.stringify({ logs: serviceLog }));
407 } catch (e) {
408 console.log(e);
409 resp.status(500);
410 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
411 } finally {
412 resp.end();
413 }
414};
415
gio7d813702025-05-08 18:29:52 +0000416const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000417 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000418 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000419 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000420});
421
422const handleRegisterWorker: express.Handler = async (req, resp) => {
423 try {
424 const projectId = Number(req.params["projectId"]);
425
426 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000427 if (!result.success) {
428 resp.status(400);
429 resp.write(
430 JSON.stringify({
431 error: "Invalid request data",
432 details: result.error.format(),
433 }),
434 );
435 return;
436 }
437
gio3a921b82025-05-10 07:36:09 +0000438 console.log(result);
439 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000440
441 // Get existing workers or initialize empty array
442 const projectWorkers = workers.get(projectId) || [];
443
444 // Add new worker if not already present
445 if (!projectWorkers.includes(address)) {
446 projectWorkers.push(address);
447 }
448
449 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000450 if (log) {
451 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
452 svcLogs.set(service, log);
453 logs.set(projectId, svcLogs);
454 }
gio7d813702025-05-08 18:29:52 +0000455 resp.status(200);
456 resp.write(
457 JSON.stringify({
458 success: true,
gio7d813702025-05-08 18:29:52 +0000459 }),
460 );
461 } catch (e) {
462 console.log(e);
463 resp.status(500);
464 resp.write(JSON.stringify({ error: "Failed to register worker" }));
465 } finally {
466 resp.end();
467 }
468};
469
470const handleReload: express.Handler = async (req, resp) => {
471 try {
472 const projectId = Number(req.params["projectId"]);
473 const projectWorkers = workers.get(projectId) || [];
474
475 if (projectWorkers.length === 0) {
476 resp.status(404);
477 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
478 return;
479 }
480
481 await Promise.all(
482 projectWorkers.map(async (workerAddress) => {
483 try {
484 const updateEndpoint = `${workerAddress}/update`;
485 await axios.post(updateEndpoint);
gio0b4002c2025-05-11 15:48:51 +0000486 // eslint-disable-next-line @typescript-eslint/no-explicit-any
487 } catch (error: any) {
gio7d813702025-05-08 18:29:52 +0000488 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
489 }
490 }),
491 );
492
493 resp.status(200);
494 resp.write(JSON.stringify({ success: true }));
495 } catch (e) {
496 console.log(e);
497 resp.status(500);
498 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
499 } finally {
500 resp.end();
501 }
502};
503
giod0026612025-05-08 13:00:36 +0000504async function start() {
505 await db.$connect();
506 const app = express();
507 app.use(express.json());
508 app.post("/api/project/:projectId/saved", handleSave);
509 app.get("/api/project/:projectId/saved", handleSavedGet);
510 app.post("/api/project/:projectId/deploy", handleDeploy);
511 app.get("/api/project/:projectId/status", handleStatus);
512 app.delete("/api/project/:projectId", handleDelete);
513 app.get("/api/project", handleProjectAll);
514 app.post("/api/project", handleProjectCreate);
515 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
516 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
517 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000518 app.post("/api/project/:projectId/workers", handleRegisterWorker);
519 app.post("/api/project/:projectId/reload", handleReload);
gio3a921b82025-05-10 07:36:09 +0000520 app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
giod0026612025-05-08 13:00:36 +0000521 app.use("/", express.static("../front/dist"));
522 app.listen(env.DODO_PORT_WEB, () => {
523 console.log("started");
524 });
525}
526
527start();