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