blob: 53fd64cc9681d9b2432db24709a4607cd104c57e [file] [log] [blame]
gioc31bf142025-06-16 07:48:20 +00001import {
2 AppNode,
3 BoundEnvVar,
4 Env,
5 GatewayHttpsNode,
6 GatewayTCPNode,
7 MongoDBNode,
8 Network,
9 NetworkNode,
10 Port,
11 PostgreSQLNode,
12 ServiceNode,
13 VolumeNode,
14} from "./graph.js";
15import { Edge } from "@xyflow/react";
16import { v4 as uuidv4 } from "uuid";
17import { ConfigWithInput, Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain } from "./types.js";
18
19export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): ConfigWithInput | null {
20 try {
21 if (appId == null || env.managerAddr == null) {
22 return null;
23 }
24 const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
25 const ingressNodes = nodes
26 .filter((n) => n.type === "gateway-https")
27 .filter((n) => n.data.https !== undefined && !n.data.readonly);
28 const tcpNodes = nodes
29 .filter((n) => n.type === "gateway-tcp")
30 .filter((n) => n.data.exposed !== undefined && !n.data.readonly);
31 const findExpose = (n: AppNode): PortDomain[] => {
32 return n.data.ports
33 .map((p) => [n.id, p.id, p.name])
34 .flatMap((sp) => {
35 return tcpNodes.flatMap((i) =>
36 (i.data.exposed || [])
37 .filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
38 .map(() => ({
39 nodeId: i.id,
40 network: networkMap.get(i.data.network!)!,
41 subdomain: i.data.subdomain!,
42 port: { name: sp[2] },
43 })),
44 );
45 });
46 };
47 return {
48 input: {
49 appId: appId,
50 managerAddr: env.managerAddr,
51 },
52 service: nodes
53 .filter((n) => n.type === "app")
54 .map((n): Service => {
55 return {
56 nodeId: n.id,
57 type: n.data.type,
58 name: n.data.label,
59 source: {
60 repository: nodes
61 .filter((i) => i.type === "github")
62 .find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
63 branch:
64 n.data.repository != undefined && "branch" in n.data.repository
65 ? n.data.repository.branch
66 : "main",
67 rootDir:
68 n.data.repository != undefined && "rootDir" in n.data.repository
69 ? n.data.repository.rootDir
70 : "/",
71 },
72 ports: (n.data.ports || [])
73 .filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
74 .map((p) => ({
75 name: p.name.toLowerCase(),
76 value: p.value,
77 protocol: "TCP", // TODO(gio)
78 })),
79 env: (n.data.envVars || [])
80 .filter((e) => "name" in e)
81 .map((e) => ({
82 name: e.name,
83 alias: "alias" in e ? e.alias : undefined,
84 })),
85 ingress: ingressNodes
86 .filter((i) => i.data.https!.serviceId === n.id)
87 .map(
88 (i): Ingress => ({
89 nodeId: i.id,
90 network: networkMap.get(i.data.network!)!,
91 subdomain: i.data.subdomain!,
92 port: {
93 name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
94 },
95 auth:
96 i.data.auth?.enabled || false
97 ? {
98 enabled: true,
99 groups: i.data.auth!.groups,
100 noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
101 }
102 : {
103 enabled: false,
104 },
105 }),
106 ),
107 expose: findExpose(n),
108 preBuildCommands: n.data.preBuildCommands
109 ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
110 : [],
111 dev: {
112 enabled: n.data.dev ? n.data.dev.enabled : false,
113 username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
114 codeServer:
115 n.data.dev?.enabled && n.data.dev.expose != null
116 ? {
117 network: networkMap.get(n.data.dev.expose.network)!,
118 subdomain: n.data.dev.expose.subdomain,
119 }
120 : undefined,
121 ssh:
122 n.data.dev?.enabled && n.data.dev.expose != null
123 ? {
124 network: networkMap.get(n.data.dev.expose.network)!,
125 subdomain: n.data.dev.expose.subdomain,
126 }
127 : undefined,
128 },
129 };
130 }),
131 volume: nodes
132 .filter((n) => n.type === "volume")
133 .map(
134 (n): Volume => ({
135 nodeId: n.id,
136 name: n.data.label,
137 accessMode: n.data.type,
138 size: n.data.size,
139 }),
140 ),
141 postgresql: nodes
142 .filter((n) => n.type === "postgresql")
143 .map(
144 (n): PostgreSQL => ({
145 nodeId: n.id,
146 name: n.data.label,
147 size: "1Gi", // TODO(gio)
148 expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
149 }),
150 ),
151 mongodb: nodes
152 .filter((n) => n.type === "mongodb")
153 .map(
154 (n): MongoDB => ({
155 nodeId: n.id,
156 name: n.data.label,
157 size: "1Gi", // TODO(gio)
158 expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
159 }),
160 ),
161 };
162 } catch (e) {
163 console.log(e);
164 return { input: { appId: "qweqwe", managerAddr: "" } };
165 }
166}
167
168export type Graph = {
169 nodes: AppNode[];
170 edges: Edge[];
171};
172
173export function configToGraph(config: Config, networks: Network[], current?: Graph): Graph {
174 if (current == null) {
175 current = { nodes: [], edges: [] };
176 }
177 const ret: Graph = {
178 nodes: [],
179 edges: [],
180 };
181 if (networks.length === 0) {
182 return ret;
183 }
184 const networkNodes = networks.map((n): NetworkNode => {
185 let existing: NetworkNode | undefined = undefined;
186 existing = current.nodes
187 .filter((i): i is NetworkNode => i.type === "network")
188 .find((i) => i.data.domain === n.domain);
189 return {
190 id: n.domain,
191 type: "network",
192 data: {
193 label: n.name,
194 domain: n.domain,
195 envVars: [],
196 ports: [],
197 },
198 position: existing != null ? existing.position : { x: 0, y: 0 },
199 };
200 });
201 const services = config.service?.map((s): ServiceNode => {
202 let existing: ServiceNode | null = null;
203 if (s.nodeId !== undefined) {
204 existing = current.nodes.find((n) => n.id === s.nodeId) as ServiceNode;
205 }
206 return {
207 id: existing != null ? existing.id : uuidv4(),
208 type: "app",
209 data: {
210 label: s.name,
211 type: s.type,
212 env: [],
213 ports: (s.ports || []).map(
214 (p): Port => ({
215 id: uuidv4(),
216 name: p.name,
217 value: p.value,
218 }),
219 ),
220 envVars: (s.env || []).map((e): BoundEnvVar => {
221 if (e.alias != null) {
222 return {
223 id: uuidv4(),
224 name: e.name,
225 source: null,
226 alias: e.alias,
227 isEditting: false,
228 };
229 } else {
230 return {
231 id: uuidv4(),
232 name: e.name,
233 source: null,
234 isEditting: false,
235 };
236 }
237 }),
238 volume: s.volume || [],
239 preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
240 // TODO(gio): dev
241 isChoosingPortToConnect: false,
242 },
243 // TODO(gio): generate position
244 position:
245 existing != null
246 ? existing.position
247 : {
248 x: 0,
249 y: 0,
250 },
251 };
252 });
253 const serviceGateways = config.service?.flatMap((s, index): GatewayHttpsNode[] => {
254 return (s.ingress || []).map((i): GatewayHttpsNode => {
255 let existing: GatewayHttpsNode | null = null;
256 if (i.nodeId !== undefined) {
257 existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
258 }
259 console.log("!!!", i.network, networks);
260 return {
261 id: existing != null ? existing.id : uuidv4(),
262 type: "gateway-https",
263 data: {
264 label: i.subdomain,
265 envVars: [],
266 ports: [],
267 network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
268 subdomain: i.subdomain,
269 https: {
270 serviceId: services![index]!.id,
271 portId: services![index]!.data.ports.find((p) => {
272 const port = i.port;
273 if ("name" in port) {
274 return p.name === port.name;
275 } else {
276 return `${p.value}` === port.value;
277 }
278 })!.id,
279 },
280 auth: i.auth.enabled
281 ? {
282 enabled: true,
283 groups: i.auth.groups || [],
284 noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
285 }
286 : {
287 enabled: false,
288 groups: [],
289 noAuthPathPatterns: [],
290 },
291 },
292 position: {
293 x: 0,
294 y: 0,
295 },
296 };
297 });
298 });
299 const exposures = new Map<string, GatewayTCPNode>();
300 config.service
301 ?.flatMap((s, index): GatewayTCPNode[] => {
302 return (s.expose || []).map((e): GatewayTCPNode => {
303 let existing: GatewayTCPNode | null = null;
304 if (e.nodeId !== undefined) {
305 existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
306 }
307 return {
308 id: existing != null ? existing.id : uuidv4(),
309 type: "gateway-tcp",
310 data: {
311 label: e.subdomain,
312 envVars: [],
313 ports: [],
314 network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
315 subdomain: e.subdomain,
316 exposed: [
317 {
318 serviceId: services![index]!.id,
319 portId: services![index]!.data.ports.find((p) => {
320 const port = e.port;
321 if ("name" in port) {
322 return p.name === port.name;
323 } else {
324 return p.value === port.value;
325 }
326 })!.id,
327 },
328 ],
329 },
330 position: existing != null ? existing.position : { x: 0, y: 0 },
331 };
332 });
333 })
334 .forEach((n) => {
335 const key = `${n.data.network}-${n.data.subdomain}`;
336 if (!exposures.has(key)) {
337 exposures.set(key, n);
338 } else {
339 exposures.get(key)!.data.exposed.push(...n.data.exposed);
340 }
341 });
342 const volumes = config.volume?.map((v): VolumeNode => {
343 let existing: VolumeNode | null = null;
344 if (v.nodeId !== undefined) {
345 existing = current.nodes.find((n) => n.id === v.nodeId) as VolumeNode;
346 }
347 return {
348 id: existing != null ? existing.id : uuidv4(),
349 type: "volume",
350 data: {
351 label: v.name,
352 type: v.accessMode,
353 size: v.size,
354 attachedTo: [],
355 envVars: [],
356 ports: [],
357 },
358 position:
359 existing != null
360 ? existing.position
361 : {
362 x: 0,
363 y: 0,
364 },
365 };
366 });
367 const postgresql = config.postgresql?.map((p): PostgreSQLNode => {
368 let existing: PostgreSQLNode | null = null;
369 if (p.nodeId !== undefined) {
370 existing = current.nodes.find((n) => n.id === p.nodeId) as PostgreSQLNode;
371 }
372 return {
373 id: existing != null ? existing.id : uuidv4(),
374 type: "postgresql",
375 data: {
376 label: p.name,
377 volumeId: "", // TODO(gio): volume
378 envVars: [],
379 ports: [
380 {
381 id: "connection",
382 name: "connection",
383 value: 5432,
384 },
385 ],
386 },
387 position:
388 existing != null
389 ? existing.position
390 : {
391 x: 0,
392 y: 0,
393 },
394 };
395 });
396 config.postgresql
397 ?.flatMap((p, index): GatewayTCPNode[] => {
398 return (p.expose || []).map((e): GatewayTCPNode => {
399 let existing: GatewayTCPNode | null = null;
400 if (e.nodeId !== undefined) {
401 existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
402 }
403 return {
404 id: existing != null ? existing.id : uuidv4(),
405 type: "gateway-tcp",
406 data: {
407 label: e.subdomain,
408 envVars: [],
409 ports: [],
410 network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
411 subdomain: e.subdomain,
412 exposed: [
413 {
414 serviceId: postgresql![index]!.id,
415 portId: "connection",
416 },
417 ],
418 },
419 position: existing != null ? existing.position : { x: 0, y: 0 },
420 };
421 });
422 })
423 .forEach((n) => {
424 const key = `${n.data.network}-${n.data.subdomain}`;
425 if (!exposures.has(key)) {
426 exposures.set(key, n);
427 } else {
428 exposures.get(key)!.data.exposed.push(...n.data.exposed);
429 }
430 });
431 const mongodb = config.mongodb?.map((m): MongoDBNode => {
432 let existing: MongoDBNode | null = null;
433 if (m.nodeId !== undefined) {
434 existing = current.nodes.find((n) => n.id === m.nodeId) as MongoDBNode;
435 }
436 return {
437 id: existing != null ? existing.id : uuidv4(),
438 type: "mongodb",
439 data: {
440 label: m.name,
441 volumeId: "", // TODO(gio): volume
442 envVars: [],
443 ports: [
444 {
445 id: "connection",
446 name: "connection",
447 value: 27017,
448 },
449 ],
450 },
451 position:
452 existing != null
453 ? existing.position
454 : {
455 x: 0,
456 y: 0,
457 },
458 };
459 });
460 config.mongodb
461 ?.flatMap((p, index): GatewayTCPNode[] => {
462 return (p.expose || []).map((e): GatewayTCPNode => {
463 let existing: GatewayTCPNode | null = null;
464 if (e.nodeId !== undefined) {
465 existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
466 }
467 return {
468 id: existing != null ? existing.id : uuidv4(),
469 type: "gateway-tcp",
470 data: {
471 label: e.subdomain,
472 envVars: [],
473 ports: [],
474 network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
475 subdomain: e.subdomain,
476 exposed: [
477 {
478 serviceId: mongodb![index]!.id,
479 portId: "connection",
480 },
481 ],
482 },
483 position: existing != null ? existing.position : { x: 0, y: 0 },
484 };
485 });
486 })
487 .forEach((n) => {
488 const key = `${n.data.network}-${n.data.subdomain}`;
489 if (!exposures.has(key)) {
490 exposures.set(key, n);
491 } else {
492 exposures.get(key)!.data.exposed.push(...n.data.exposed);
493 }
494 });
495 ret.nodes = [
496 ...networkNodes,
497 ...ret.nodes,
498 ...(services || []),
499 ...(serviceGateways || []),
500 ...(volumes || []),
501 ...(postgresql || []),
502 ...(mongodb || []),
503 ...(exposures.values() || []),
504 ];
505 services?.forEach((s) => {
506 s.data.envVars.forEach((e) => {
507 if (!("name" in e)) {
508 return;
509 }
510 if (!e.name.startsWith("DODO_")) {
511 return;
512 }
513 let r: {
514 type: string;
515 name: string;
516 } | null = null;
517 if (e.name.startsWith("DODO_PORT_")) {
518 return;
519 } else if (e.name.startsWith("DODO_POSTGRESQL_")) {
520 r = {
521 type: "postgresql",
522 name: e.name.replace("DODO_POSTGRESQL_", "").replace("_URL", "").toLowerCase(),
523 };
524 } else if (e.name.startsWith("DODO_MONGODB_")) {
525 r = {
526 type: "mongodb",
527 name: e.name.replace("DODO_MONGODB_", "").replace("_URL", "").toLowerCase(),
528 };
529 } else if (e.name.startsWith("DODO_VOLUME_")) {
530 r = {
531 type: "volume",
532 name: e.name.replace("DODO_VOLUME_", "").toLowerCase(),
533 };
534 }
535 if (r != null) {
536 e.source = ret.nodes.find((n) => n.type === r.type && n.data.label.toLowerCase() === r.name)!.id;
537 }
538 });
539 });
540 const envVarEdges = [...(services || [])].flatMap((n): Edge[] => {
541 return n.data.envVars.flatMap((e): Edge[] => {
542 if (e.source == null) {
543 return [];
544 }
545 const sn = ret.nodes.find((n) => n.id === e.source!)!;
546 const sourceHandle = sn.type === "app" ? "ports" : sn.type === "volume" ? "volume" : "env_var";
547 return [
548 {
549 id: uuidv4(),
550 source: e.source!,
551 sourceHandle: sourceHandle,
552 target: n.id,
553 targetHandle: "env_var",
554 },
555 ];
556 });
557 });
558 const exposureEdges = [...exposures.values()].flatMap((n): Edge[] => {
559 return n.data.exposed.flatMap((e): Edge[] => {
560 return [
561 {
562 id: uuidv4(),
563 source: e.serviceId,
564 sourceHandle: ret.nodes.find((n) => n.id === e.serviceId)!.type === "app" ? "ports" : "env_var",
565 target: n.id,
566 targetHandle: "tcp",
567 },
568 {
569 id: uuidv4(),
570 source: n.id,
571 sourceHandle: "subdomain",
572 target: n.data.network!,
573 targetHandle: "subdomain",
574 },
575 ];
576 });
577 });
578 const ingressEdges = [...(serviceGateways || [])].flatMap((n): Edge[] => {
579 return [
580 {
581 id: uuidv4(),
582 source: n.data.https!.serviceId,
583 sourceHandle: "ports",
584 target: n.id,
585 targetHandle: "https",
586 },
587 {
588 id: uuidv4(),
589 source: n.id,
590 sourceHandle: "subdomain",
591 target: n.data.network!,
592 targetHandle: "subdomain",
593 },
594 ];
595 });
596 ret.edges = [...envVarEdges, ...exposureEdges, ...ingressEdges];
597 return ret;
598}