blob: c934a9b5c94afaf17d28086f319a18827541d177 [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()));
205 const githubNodes = stateObj.nodes.filter(
206 (n: any) => n.type === "github" && n.data?.repository?.id,
207 );
208
209 const github = new GithubClient(p.githubToken);
210 for (const node of githubNodes) {
211 try {
212 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
213 } catch (error) {
214 console.error(
215 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
216 error,
217 );
218 }
219 }
220 }
221 }
222 } else {
223 r = await axios.request({
224 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
225 method: "put",
226 data: {
227 config: req.body.config,
228 },
229 });
230 if (r.status === 200) {
231 await db.project.update({
232 where: {
233 id: projectId,
234 },
235 data: {
236 state,
237 draft: null,
238 },
239 });
240 }
241 }
242 } catch (e) {
243 console.log(e);
244 resp.status(500);
245 } finally {
246 resp.end();
247 }
248};
249
250const handleStatus: express.Handler = async (req, resp) => {
251 try {
252 const projectId = Number(req.params["projectId"]);
253 const p = await db.project.findUnique({
254 where: {
255 id: projectId,
256 },
257 select: {
258 instanceId: true,
259 },
260 });
261 console.log(projectId, p);
262 if (p === null) {
263 resp.status(404);
264 return;
265 }
266 if (p.instanceId == null) {
267 resp.status(404);
268 return;
269 }
270 const r = await axios.request({
271 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
272 method: "get",
273 });
274 resp.status(r.status);
275 if (r.status === 200) {
276 resp.write(JSON.stringify(r.data));
277 }
278 } catch (e) {
279 console.log(e);
280 resp.status(500);
281 } finally {
282 resp.end();
283 }
284};
285
286const handleGithubRepos: express.Handler = async (req, resp) => {
287 try {
288 const projectId = Number(req.params["projectId"]);
289 const project = await db.project.findUnique({
290 where: { id: projectId },
291 select: { githubToken: true },
292 });
293
294 if (!project?.githubToken) {
295 resp.status(400);
296 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
297 return;
298 }
299
300 const github = new GithubClient(project.githubToken);
301 const repositories = await github.getRepositories();
302
303 resp.status(200);
304 resp.header("Content-Type", "application/json");
305 resp.write(JSON.stringify(repositories));
306 } catch (e) {
307 console.log(e);
308 resp.status(500);
309 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
310 } finally {
311 resp.end();
312 }
313};
314
315const handleUpdateGithubToken: express.Handler = async (req, resp) => {
316 try {
317 const projectId = Number(req.params["projectId"]);
318 const { githubToken } = req.body;
319
320 await db.project.update({
321 where: { id: projectId },
322 data: { githubToken },
323 });
324
325 resp.status(200);
326 } catch (e) {
327 console.log(e);
328 resp.status(500);
329 } finally {
330 resp.end();
331 }
332};
333
334const handleEnv: express.Handler = async (req, resp) => {
335 const projectId = Number(req.params["projectId"]);
336 try {
337 const project = await db.project.findUnique({
338 where: { id: projectId },
339 select: {
340 deployKey: true,
341 githubToken: true,
342 },
343 });
344
345 if (!project) {
346 resp.status(404);
347 resp.write(JSON.stringify({ error: "Project not found" }));
348 return;
349 }
350
gio3a921b82025-05-10 07:36:09 +0000351 const projectLogs = logs.get(projectId) || new Map();
352 const services = Array.from(projectLogs.keys());
353
giod0026612025-05-08 13:00:36 +0000354 resp.status(200);
355 resp.write(
356 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000357 // TODO(gio): get from env or command line flags
358 managerAddr: "http://10.42.0.239:8080",
giod0026612025-05-08 13:00:36 +0000359 deployKey: project.deployKey,
360 integrations: {
361 github: !!project.githubToken,
362 },
363 networks: [
364 {
365 name: "Public",
366 domain: "v1.dodo.cloud",
367 },
368 {
369 name: "Private",
370 domain: "p.v1.dodo.cloud",
371 },
372 ],
gio3a921b82025-05-10 07:36:09 +0000373 services,
giod0026612025-05-08 13:00:36 +0000374 }),
375 );
376 } catch (error) {
377 console.error("Error checking integrations:", error);
378 resp.status(500);
379 resp.write(JSON.stringify({ error: "Internal server error" }));
380 } finally {
381 resp.end();
382 }
383};
384
gio3a921b82025-05-10 07:36:09 +0000385const handleServiceLogs: express.Handler = async (req, resp) => {
386 try {
387 const projectId = Number(req.params["projectId"]);
388 const service = req.params["service"];
389
390 const projectLogs = logs.get(projectId);
391 if (!projectLogs) {
392 resp.status(404);
393 resp.write(JSON.stringify({ error: "No logs found for this project" }));
394 return;
395 }
396
397 const serviceLog = projectLogs.get(service);
398 if (!serviceLog) {
399 resp.status(404);
400 resp.write(JSON.stringify({ error: "No logs found for this service" }));
401 return;
402 }
403
404 resp.status(200);
405 resp.write(JSON.stringify({ logs: serviceLog }));
406 } catch (e) {
407 console.log(e);
408 resp.status(500);
409 resp.write(JSON.stringify({ error: "Failed to get service logs" }));
410 } finally {
411 resp.end();
412 }
413};
414
gio7d813702025-05-08 18:29:52 +0000415const WorkerSchema = z.object({
gio3a921b82025-05-10 07:36:09 +0000416 service: z.string(),
gio7d813702025-05-08 18:29:52 +0000417 address: z.string().url(),
gio3a921b82025-05-10 07:36:09 +0000418 logs: z.optional(z.string()),
gio7d813702025-05-08 18:29:52 +0000419});
420
421const handleRegisterWorker: express.Handler = async (req, resp) => {
422 try {
423 const projectId = Number(req.params["projectId"]);
424
425 const result = WorkerSchema.safeParse(req.body);
gio7d813702025-05-08 18:29:52 +0000426 if (!result.success) {
427 resp.status(400);
428 resp.write(
429 JSON.stringify({
430 error: "Invalid request data",
431 details: result.error.format(),
432 }),
433 );
434 return;
435 }
436
gio3a921b82025-05-10 07:36:09 +0000437 console.log(result);
438 const { service, address, logs: log } = result.data;
gio7d813702025-05-08 18:29:52 +0000439
440 // Get existing workers or initialize empty array
441 const projectWorkers = workers.get(projectId) || [];
442
443 // Add new worker if not already present
444 if (!projectWorkers.includes(address)) {
445 projectWorkers.push(address);
446 }
447
448 workers.set(projectId, projectWorkers);
gio3a921b82025-05-10 07:36:09 +0000449 if (log) {
450 const svcLogs: Map<string, string> = logs.get(projectId) || new Map();
451 svcLogs.set(service, log);
452 logs.set(projectId, svcLogs);
453 }
gio7d813702025-05-08 18:29:52 +0000454 resp.status(200);
455 resp.write(
456 JSON.stringify({
457 success: true,
gio7d813702025-05-08 18:29:52 +0000458 }),
459 );
460 } catch (e) {
461 console.log(e);
462 resp.status(500);
463 resp.write(JSON.stringify({ error: "Failed to register worker" }));
464 } finally {
465 resp.end();
466 }
467};
468
469const handleReload: express.Handler = async (req, resp) => {
470 try {
471 const projectId = Number(req.params["projectId"]);
472 const projectWorkers = workers.get(projectId) || [];
473
474 if (projectWorkers.length === 0) {
475 resp.status(404);
476 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
477 return;
478 }
479
480 await Promise.all(
481 projectWorkers.map(async (workerAddress) => {
482 try {
483 const updateEndpoint = `${workerAddress}/update`;
484 await axios.post(updateEndpoint);
485 } catch (error: any) {
486 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
487 }
488 }),
489 );
490
491 resp.status(200);
492 resp.write(JSON.stringify({ success: true }));
493 } catch (e) {
494 console.log(e);
495 resp.status(500);
496 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
497 } finally {
498 resp.end();
499 }
500};
501
giod0026612025-05-08 13:00:36 +0000502async function start() {
503 await db.$connect();
504 const app = express();
505 app.use(express.json());
506 app.post("/api/project/:projectId/saved", handleSave);
507 app.get("/api/project/:projectId/saved", handleSavedGet);
508 app.post("/api/project/:projectId/deploy", handleDeploy);
509 app.get("/api/project/:projectId/status", handleStatus);
510 app.delete("/api/project/:projectId", handleDelete);
511 app.get("/api/project", handleProjectAll);
512 app.post("/api/project", handleProjectCreate);
513 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
514 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
515 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000516 app.post("/api/project/:projectId/workers", handleRegisterWorker);
517 app.post("/api/project/:projectId/reload", handleReload);
gio3a921b82025-05-10 07:36:09 +0000518 app.get("/api/project/:projectId/logs/:service", handleServiceLogs);
giod0026612025-05-08 13:00:36 +0000519 app.use("/", express.static("../front/dist"));
520 app.listen(env.DODO_PORT_WEB, () => {
521 console.log("started");
522 });
523}
524
525start();