blob: 0a35dfe5f324b0ae1e918a4f78dd335e22ce03bd [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[]>();
12
giod0026612025-05-08 13:00:36 +000013const handleProjectCreate: express.Handler = async (req, resp) => {
14 try {
15 const { id } = await db.project.create({
16 data: {
17 userId: "gio", // req.get("x-forwarded-userid")!,
18 name: req.body.name,
19 },
20 });
21 resp.status(200);
22 resp.header("Content-Type", "application/json");
23 resp.write(
24 JSON.stringify({
25 id,
26 }),
27 );
28 } catch (e) {
29 console.log(e);
30 resp.status(500);
31 } finally {
32 resp.end();
33 }
34};
35
36const handleProjectAll: express.Handler = async (req, resp) => {
37 try {
38 const r = await db.project.findMany({
39 where: {
40 userId: "gio", // req.get("x-forwarded-userid")!,
41 },
42 });
43 resp.status(200);
44 resp.header("Content-Type", "application/json");
45 resp.write(
46 JSON.stringify(
47 r.map((p) => ({
48 id: p.id.toString(),
49 name: p.name,
50 })),
51 ),
52 );
53 } catch (e) {
54 console.log(e);
55 resp.status(500);
56 } finally {
57 resp.end();
58 }
59};
60
61const handleSave: express.Handler = async (req, resp) => {
62 try {
63 await db.project.update({
64 where: {
65 id: Number(req.params["projectId"]),
66 },
67 data: {
68 draft: Buffer.from(JSON.stringify(req.body)),
69 },
70 });
71 resp.status(200);
72 } catch (e) {
73 console.log(e);
74 resp.status(500);
75 } finally {
76 resp.end();
77 }
78};
79
80const handleSavedGet: express.Handler = async (req, resp) => {
81 try {
82 const r = await db.project.findUnique({
83 where: {
84 id: Number(req.params["projectId"]),
85 },
86 select: {
87 state: true,
88 draft: true,
89 },
90 });
91 if (r == null) {
92 resp.status(404);
93 } else {
94 resp.status(200);
95 resp.header("content-type", "application/json");
96 if (r.draft == null) {
97 if (r.state == null) {
98 resp.send({
99 nodes: [],
100 edges: [],
101 viewport: { x: 0, y: 0, zoom: 1 },
102 });
103 } else {
104 resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
105 }
106 } else {
107 resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
108 }
109 }
110 } catch (e) {
111 console.log(e);
112 resp.status(500);
113 } finally {
114 resp.end();
115 }
116};
117
118const handleDelete: express.Handler = async (req, resp) => {
119 try {
120 const projectId = Number(req.params["projectId"]);
121 const p = await db.project.findUnique({
122 where: {
123 id: projectId,
124 },
125 select: {
126 instanceId: true,
127 },
128 });
129 if (p === null) {
130 resp.status(404);
131 return;
132 }
133 const r = await axios.request({
134 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/remove`,
135 method: "post",
136 });
137 if (r.status === 200) {
138 await db.project.delete({
139 where: {
140 id: projectId,
141 },
142 });
143 }
144 resp.status(200);
145 } catch (e) {
146 console.log(e);
147 resp.status(500);
148 } finally {
149 resp.end();
150 }
151};
152
153const handleDeploy: express.Handler = async (req, resp) => {
154 try {
155 const projectId = Number(req.params["projectId"]);
156 const state = Buffer.from(JSON.stringify(req.body.state));
157 const p = await db.project.findUnique({
158 where: {
159 id: projectId,
160 },
161 select: {
162 instanceId: true,
163 githubToken: true,
164 deployKey: true,
165 },
166 });
167 if (p === null) {
168 resp.status(404);
169 return;
170 }
171 await db.project.update({
172 where: {
173 id: projectId,
174 },
175 data: {
176 draft: state,
177 },
178 });
179 let r: { status: number; data: { id: string; deployKey: string } };
180 if (p.instanceId == null) {
181 r = await axios.request({
182 url: "http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app",
183 method: "post",
184 data: {
185 config: req.body.config,
186 },
187 });
188 console.log(r);
189 if (r.status === 200) {
190 await db.project.update({
191 where: {
192 id: projectId,
193 },
194 data: {
195 state,
196 draft: null,
197 instanceId: r.data.id,
198 deployKey: r.data.deployKey,
199 },
200 });
201
202 if (p.githubToken && r.data.deployKey) {
203 const stateObj = JSON.parse(JSON.parse(state.toString()));
204 const githubNodes = stateObj.nodes.filter(
205 (n: any) => n.type === "github" && n.data?.repository?.id,
206 );
207
208 const github = new GithubClient(p.githubToken);
209 for (const node of githubNodes) {
210 try {
211 await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
212 } catch (error) {
213 console.error(
214 `Failed to add deploy key to repository ${node.data.repository.sshURL}:`,
215 error,
216 );
217 }
218 }
219 }
220 }
221 } else {
222 r = await axios.request({
223 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/${p.instanceId}`,
224 method: "put",
225 data: {
226 config: req.body.config,
227 },
228 });
229 if (r.status === 200) {
230 await db.project.update({
231 where: {
232 id: projectId,
233 },
234 data: {
235 state,
236 draft: null,
237 },
238 });
239 }
240 }
241 } catch (e) {
242 console.log(e);
243 resp.status(500);
244 } finally {
245 resp.end();
246 }
247};
248
249const handleStatus: express.Handler = async (req, resp) => {
250 try {
251 const projectId = Number(req.params["projectId"]);
252 const p = await db.project.findUnique({
253 where: {
254 id: projectId,
255 },
256 select: {
257 instanceId: true,
258 },
259 });
260 console.log(projectId, p);
261 if (p === null) {
262 resp.status(404);
263 return;
264 }
265 if (p.instanceId == null) {
266 resp.status(404);
267 return;
268 }
269 const r = await axios.request({
270 url: `http://appmanager.hgrz-appmanager.svc.cluster.local/api/instance/${p.instanceId}/status`,
271 method: "get",
272 });
273 resp.status(r.status);
274 if (r.status === 200) {
275 resp.write(JSON.stringify(r.data));
276 }
277 } catch (e) {
278 console.log(e);
279 resp.status(500);
280 } finally {
281 resp.end();
282 }
283};
284
285const handleGithubRepos: express.Handler = async (req, resp) => {
286 try {
287 const projectId = Number(req.params["projectId"]);
288 const project = await db.project.findUnique({
289 where: { id: projectId },
290 select: { githubToken: true },
291 });
292
293 if (!project?.githubToken) {
294 resp.status(400);
295 resp.write(JSON.stringify({ error: "GitHub token not configured" }));
296 return;
297 }
298
299 const github = new GithubClient(project.githubToken);
300 const repositories = await github.getRepositories();
301
302 resp.status(200);
303 resp.header("Content-Type", "application/json");
304 resp.write(JSON.stringify(repositories));
305 } catch (e) {
306 console.log(e);
307 resp.status(500);
308 resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
309 } finally {
310 resp.end();
311 }
312};
313
314const handleUpdateGithubToken: express.Handler = async (req, resp) => {
315 try {
316 const projectId = Number(req.params["projectId"]);
317 const { githubToken } = req.body;
318
319 await db.project.update({
320 where: { id: projectId },
321 data: { githubToken },
322 });
323
324 resp.status(200);
325 } catch (e) {
326 console.log(e);
327 resp.status(500);
328 } finally {
329 resp.end();
330 }
331};
332
333const handleEnv: express.Handler = async (req, resp) => {
334 const projectId = Number(req.params["projectId"]);
335 try {
336 const project = await db.project.findUnique({
337 where: { id: projectId },
338 select: {
339 deployKey: true,
340 githubToken: true,
341 },
342 });
343
344 if (!project) {
345 resp.status(404);
346 resp.write(JSON.stringify({ error: "Project not found" }));
347 return;
348 }
349
350 resp.status(200);
351 resp.write(
352 JSON.stringify({
gio7d813702025-05-08 18:29:52 +0000353 // TODO(gio): get from env or command line flags
354 managerAddr: "http://10.42.0.239:8080",
giod0026612025-05-08 13:00:36 +0000355 deployKey: project.deployKey,
356 integrations: {
357 github: !!project.githubToken,
358 },
359 networks: [
360 {
361 name: "Public",
362 domain: "v1.dodo.cloud",
363 },
364 {
365 name: "Private",
366 domain: "p.v1.dodo.cloud",
367 },
368 ],
369 }),
370 );
371 } catch (error) {
372 console.error("Error checking integrations:", error);
373 resp.status(500);
374 resp.write(JSON.stringify({ error: "Internal server error" }));
375 } finally {
376 resp.end();
377 }
378};
379
gio7d813702025-05-08 18:29:52 +0000380const WorkerSchema = z.object({
381 address: z.string().url(),
382});
383
384const handleRegisterWorker: express.Handler = async (req, resp) => {
385 try {
386 const projectId = Number(req.params["projectId"]);
387
388 const result = WorkerSchema.safeParse(req.body);
389 console.log(result);
390 if (!result.success) {
391 resp.status(400);
392 resp.write(
393 JSON.stringify({
394 error: "Invalid request data",
395 details: result.error.format(),
396 }),
397 );
398 return;
399 }
400
401 const { address } = result.data;
402
403 // Get existing workers or initialize empty array
404 const projectWorkers = workers.get(projectId) || [];
405
406 // Add new worker if not already present
407 if (!projectWorkers.includes(address)) {
408 projectWorkers.push(address);
409 }
410
411 workers.set(projectId, projectWorkers);
412
413 resp.status(200);
414 resp.write(
415 JSON.stringify({
416 success: true,
417 workers: projectWorkers,
418 }),
419 );
420 } catch (e) {
421 console.log(e);
422 resp.status(500);
423 resp.write(JSON.stringify({ error: "Failed to register worker" }));
424 } finally {
425 resp.end();
426 }
427};
428
429const handleReload: express.Handler = async (req, resp) => {
430 try {
431 const projectId = Number(req.params["projectId"]);
432 const projectWorkers = workers.get(projectId) || [];
433
434 if (projectWorkers.length === 0) {
435 resp.status(404);
436 resp.write(JSON.stringify({ error: "No workers registered for this project" }));
437 return;
438 }
439
440 await Promise.all(
441 projectWorkers.map(async (workerAddress) => {
442 try {
443 const updateEndpoint = `${workerAddress}/update`;
444 await axios.post(updateEndpoint);
445 } catch (error: any) {
446 console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
447 }
448 }),
449 );
450
451 resp.status(200);
452 resp.write(JSON.stringify({ success: true }));
453 } catch (e) {
454 console.log(e);
455 resp.status(500);
456 resp.write(JSON.stringify({ error: "Failed to reload workers" }));
457 } finally {
458 resp.end();
459 }
460};
461
giod0026612025-05-08 13:00:36 +0000462async function start() {
463 await db.$connect();
464 const app = express();
465 app.use(express.json());
466 app.post("/api/project/:projectId/saved", handleSave);
467 app.get("/api/project/:projectId/saved", handleSavedGet);
468 app.post("/api/project/:projectId/deploy", handleDeploy);
469 app.get("/api/project/:projectId/status", handleStatus);
470 app.delete("/api/project/:projectId", handleDelete);
471 app.get("/api/project", handleProjectAll);
472 app.post("/api/project", handleProjectCreate);
473 app.get("/api/project/:projectId/repos/github", handleGithubRepos);
474 app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
475 app.get("/api/project/:projectId/env", handleEnv);
gio7d813702025-05-08 18:29:52 +0000476 app.post("/api/project/:projectId/workers", handleRegisterWorker);
477 app.post("/api/project/:projectId/reload", handleReload);
giod0026612025-05-08 13:00:36 +0000478 app.use("/", express.static("../front/dist"));
479 app.listen(env.DODO_PORT_WEB, () => {
480 console.log("started");
481 });
482}
483
484start();