blob: f2a0784d4d27a06ce243403e2d3b1780bdb3aae4 [file] [log] [blame]
import { AppNode, Env, GatewayHttpsNode, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
export type AuthDisabled = {
enabled: false;
};
export type AuthEnabled = {
enabled: true;
groups: string[];
noAuthPathPatterns: string[];
};
export type Auth = AuthDisabled | AuthEnabled;
export type Ingress = {
network: string;
subdomain: string;
port: { name: string; } | { value: string; };
auth: Auth;
};
export type Domain = {
network: string;
subdomain: string;
};
export type PortValue = {
name: string;
} | {
value: number;
};
export type PortDomain = Domain & {
port: PortValue;
}
export type Service = {
type: ServiceType;
name: string;
source: {
repository: string;
branch: string;
rootDir: string;
};
ports?: {
name: string;
value: number;
protocol: "TCP" | "UDP";
}[];
env?: {
name: string;
alias?: string;
}[]
ingress?: Ingress;
expose?: PortDomain[];
volume?: string[];
};
export type Volume = {
name: string;
accessMode: VolumeType;
size: string;
};
export type PostgreSQL = {
name: string;
size: string;
expose?: Domain[];
};
export type MongoDB = {
name: string;
size: string;
expose?: Domain[];
};
export type Config = {
service?: Service[];
volume?: Volume[];
postgresql?: PostgreSQL[];
mongodb?: MongoDB[];
};
export function generateDodoConfig(nodes: AppNode[], env: Env): Config | null {
try {
const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
const ingressNodes = nodes.filter((n) => n.type === "gateway-https").filter((n) => n.data.https !== undefined);
const tcpNodes = nodes.filter((n) => n.type === "gateway-tcp").filter((n) => n.data.exposed !== undefined);
const findExpose = (n: AppNode): PortDomain[] => {
return n.data.ports.map((p) => [n.id, p.id, p.name]).flatMap((sp) => {
return tcpNodes.flatMap((i) => (i.data.exposed || []).filter((t) => t.serviceId === sp[0] && t.portId === sp[1]).map(() => ({
network: networkMap.get(i.data.network!)!,
subdomain: i.data.subdomain!,
port: { name: sp[2] },
})));
});
};
return {
service: nodes.filter((n) => n.type === "app").map((n): Service => {
return {
type: n.data.type,
name: n.data.label,
source: {
repository: nodes.filter((i) => i.type === "github").find((i) => i.id === n.data.repository.id)!.data.address,
branch: n.data.repository.branch,
rootDir: n.data.repository.rootDir,
},
ports: (n.data.ports || []).map((p) => ({
name: p.name,
value: p.value,
protocol: "TCP", // TODO(gio)
})),
env: (n.data.envVars || []).filter((e) => "name" in e).map((e) => ({
name: e.name,
alias: "alias" in e ? e.alias : undefined,
})),
ingress: ((i: GatewayHttpsNode | undefined) => {
if (i === undefined) {
return undefined;
}
return {
network: networkMap.get(i.data.network!)!,
subdomain: i.data.subdomain!,
port: {
name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
},
auth: { enabled: false },
};
})(ingressNodes.find((i) => i.data.https!.serviceId === n.id)),
expose: findExpose(n),
};
}),
volume: nodes.filter((n) => n.type === "volume").map((n): Volume => ({
name: n.data.label,
accessMode: n.data.type,
size: n.data.size,
})),
postgresql: nodes.filter((n) => n.type === "postgresql").map((n): PostgreSQL => ({
name: n.data.label,
size: "1Gi", // TODO(gio)
expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
})),
mongodb: nodes.filter((n) => n.type === "mongodb").map((n): MongoDB => ({
name: n.data.label,
size: "1Gi", // TODO(gio)
expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
})),
};
} catch (e) {
console.log(e);
return null;
}
}
export interface Validator {
(nodes: AppNode[]): Message[];
}
function CombineValidators(...v: Validator[]): Validator {
return (n) => v.flatMap((v) => v(n));
}
function MessageTypeToNumber(t: MessageType) {
switch (t) {
case "FATAL": return 0;
case "WARNING": return 1;
case "INFO": return 2;
}
}
function NodeTypeToNumber(t?: NodeType) {
switch (t) {
case "github": return 0;
case "app": return 1;
case "volume": return 2;
case "postgresql": return 3;
case "mongodb": return 4;
case "gateway-https": return 5;
case undefined: return 100;
}
}
function SortingValidator(v: Validator): Validator {
return (n) => {
const nt = new Map(n.map((n) => [n.id, NodeTypeToNumber(n.type)]))
return v(n).sort((a, b) => {
const at = MessageTypeToNumber(a.type);
const bt = MessageTypeToNumber(b.type);
if (a.nodeId === undefined && b.nodeId === undefined) {
if (at !== bt) {
return at - bt;
}
return a.id.localeCompare(b.id);
}
if (a.nodeId === undefined) {
return -1;
}
if (b.nodeId === undefined) {
return 1;
}
if (a.nodeId === b.nodeId) {
if (at !== bt) {
return at - bt;
}
return a.id.localeCompare(b.id);
}
const ant = nt.get(a.id)!;
const bnt = nt.get(b.id)!;
if (ant !== bnt) {
return ant - bnt;
}
return a.id.localeCompare(b.id);
});
};
}
export function CreateValidators(): Validator {
return SortingValidator(
CombineValidators(
EmptyValidator,
GitRepositoryValidator,
ServiceValidator,
GatewayHTTPSValidator,
GatewayTCPValidator,
)
);
}
function EmptyValidator(nodes: AppNode[]): Message[] {
if (nodes.length > 0) {
return [];
}
return [{
id: "no-nodes",
type: "FATAL",
message: "Start by importing application source code",
onHighlight: (store) => store.setHighlightCategory("repository", true),
onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
}];
}
function GitRepositoryValidator(nodes: AppNode[]): Message[] {
const git = nodes.filter((n) => n.type === "github");
const noAddress: Message[] = git.filter((n) => n.data == null || n.data.address == null || n.data.address === "").map((n) => ({
id: `${n.id}-no-address`,
type: "FATAL",
nodeId: n.id,
message: "Configure repository address",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
} satisfies Message));
const noApp = git.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id)).map((n) => ({
id: `${n.id}-no-app`,
type: "WARNING",
nodeId: n.id,
message: "Connect to service",
onHighlight: (store) => store.setHighlightCategory("Services", true),
onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
} satisfies Message));
return noAddress.concat(noApp);
}
function ServiceValidator(nodes: AppNode[]): Message[] {
const apps = nodes.filter((n) => n.type === "app");
const noName = apps.filter((n) => n.data == null || n.data.label == null || n.data.label === "").map((n): Message => ({
id: `${n.id}-no-name`,
type: "FATAL",
nodeId: n.id,
message: "Name the service",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
onClick: (store) => {
store.updateNode(n.id, { selected: true });
store.updateNodeData<"app">(n.id, {
activeField: "name" ,
});
},
}));
const noSource = apps.filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "").map((n): Message => ({
id: `${n.id}-no-repo`,
type: "FATAL",
nodeId: n.id,
message: "Connect to source repository",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
const noRuntime = apps.filter((n) => n.data == null || n.data.type == null).map((n): Message => ({
id: `${n.id}-no-runtime`,
type: "FATAL",
nodeId: n.id,
message: "Choose runtime",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
onClick: (store) => {
store.updateNode(n.id, { selected: true });
store.updateNodeData<"app">(n.id, {
activeField: "type" ,
});
},
}));
const noPorts = apps.filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0).map((n): Message => ({
id: `${n.id}-no-ports`,
type: "INFO",
nodeId: n.id,
message: "Expose ports",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
const noIngress = apps.flatMap((n): Message[] => {
if (n.data == null) {
return [];
}
return (n.data.ports || []).filter((p) => !nodes.filter((i) => i.type === "gateway-https").some((i) => {
if (i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id) {
return true;
}
return false;
})).map((p): Message => ({
id: `${n.id}-${p.id}-no-ingress`,
type: "WARNING",
nodeId: n.id,
message: `Connect to ingress: ${p.name} - ${p.value}`,
onHighlight: (store) => {
store.updateNode(n.id, { selected: true });
store.setHighlightCategory("gateways", true);
},
onLooseHighlight: (store) => {
store.updateNode(n.id, { selected: false });
store.setHighlightCategory("gateways", false);
},
}));
});
const multipleIngress = apps.filter((n) => n.data != null && n.data.ports != null).flatMap((n) => n.data.ports.map((p): Message | undefined => {
const ing = nodes.filter((i) => i.type === "gateway-https").filter((i) => i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id);
if (ing.length < 2) {
return undefined;
}
return {
id: `${n.id}-${p.id}-multiple-ingress`,
type: "FATAL",
nodeId: n.id,
message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
};
})).filter((m) => m !== undefined);
return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}
function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
const ing = nodes.filter((n) => n.type === "gateway-https");
const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
id: `${n.id}-no-network`,
type: "FATAL",
nodeId: n.id,
message: "Network and subdomain must be defined",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.https == null || n.data.https.serviceId == null || n.data.https.serviceId == "" || n.data.https.portId == null || n.data.https.portId == "").map((n) => ({
id: `${n.id}-not-connected`,
type: "FATAL",
nodeId: n.id,
message: "Connect to a service port",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
return noNetwork.concat(notConnected);
}
function GatewayTCPValidator(nodes: AppNode[]): Message[] {
const ing = nodes.filter((n) => n.type === "gateway-tcp");
const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
id: `${n.id}-no-network`,
type: "FATAL",
nodeId: n.id,
message: "Network and subdomain must be defined",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0).map((n) => ({
id: `${n.id}-not-connected`,
type: "FATAL",
nodeId: n.id,
message: "Connect to a service port",
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
}));
return noNetwork.concat(notConnected);
}