pangolin/server/lib/blueprints/parseDockerContainers.ts
2025-09-14 17:27:21 -07:00

301 lines
10 KiB
TypeScript

import logger from "@server/logger";
import { setNestedProperty } from "./parseDotNotation";
export type DockerLabels = {
[key: string]: string;
};
export type ParsedObject = {
[key: string]: any;
};
type ContainerPort = {
privatePort: number;
publicPort: number;
type: string;
ip: string;
};
type Container = {
id: string;
name: string;
image: string;
state: string;
status: string;
ports: ContainerPort[] | null;
labels: DockerLabels;
created: number;
networks: { [key: string]: any };
hostname: string;
};
type Target = {
hostname?: string;
port?: number;
method?: string;
enabled?: boolean;
[key: string]: any;
};
type ResourceConfig = {
[key: string]: any;
targets?: (Target | null)[];
};
function getContainerPort(container: Container): number | null {
if (!container.ports || container.ports.length === 0) {
return null;
}
// Return the first port's privatePort
return container.ports[0].privatePort;
// return container.ports[0].publicPort;
}
export function processContainerLabels(containers: Container[]): {
"proxy-resources": { [key: string]: ResourceConfig };
"client-resources": { [key: string]: ResourceConfig };
} {
const result = {
"proxy-resources": {} as { [key: string]: ResourceConfig },
"client-resources": {} as { [key: string]: ResourceConfig }
};
// Process each container
containers.forEach((container) => {
if (container.state !== "running") {
return;
}
const proxyResourceLabels: DockerLabels = {};
const clientResourceLabels: DockerLabels = {};
// Filter and separate proxy-resources and client-resources labels
Object.entries(container.labels).forEach(([key, value]) => {
if (key.startsWith("pangolin.proxy-resources.")) {
// remove the pangolin.proxy- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.proxy-", "");
proxyResourceLabels[strippedKey] = value;
} else if (key.startsWith("pangolin.client-resources.")) {
// remove the pangolin.client- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.client-", "");
clientResourceLabels[strippedKey] = value;
}
});
// Process proxy resources
if (Object.keys(proxyResourceLabels).length > 0) {
processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]);
}
// Process client resources
if (Object.keys(clientResourceLabels).length > 0) {
processResourceLabels(clientResourceLabels, container, result["client-resources"]);
}
});
return result;
}
function processResourceLabels(
resourceLabels: DockerLabels,
container: Container,
targetResult: { [key: string]: ResourceConfig }
) {
// Parse the labels using the existing parseDockerLabels logic
const tempResult: ParsedObject = {};
Object.entries(resourceLabels).forEach(([key, value]) => {
setNestedProperty(tempResult, key, value);
});
// Merge into target result
if (tempResult.resources) {
Object.entries(tempResult.resources).forEach(
([resourceKey, resourceConfig]: [string, any]) => {
// Initialize resource if it doesn't exist
if (!targetResult[resourceKey]) {
targetResult[resourceKey] = {};
}
// Merge all properties except targets
Object.entries(resourceConfig).forEach(
([propKey, propValue]) => {
if (propKey !== "targets") {
targetResult[resourceKey][propKey] = propValue;
}
}
);
// Handle targets specially
if (
resourceConfig.targets &&
Array.isArray(resourceConfig.targets)
) {
const resource = targetResult[resourceKey];
if (resource) {
if (!resource.targets) {
resource.targets = [];
}
resourceConfig.targets.forEach(
(target: any, targetIndex: number) => {
// check if the target is an empty object
if (
typeof target === "object" &&
Object.keys(target).length === 0
) {
logger.debug(
`Skipping null target at index ${targetIndex} for resource ${resourceKey}`
);
resource.targets!.push(null);
return;
}
// Ensure targets array is long enough
while (
resource.targets!.length <= targetIndex
) {
resource.targets!.push({});
}
// Set default hostname and port if not provided
const finalTarget = { ...target };
if (!finalTarget.hostname) {
finalTarget.hostname =
container.name ||
container.hostname;
}
if (!finalTarget.port) {
const containerPort =
getContainerPort(container);
if (containerPort !== null) {
finalTarget.port = containerPort;
}
}
// Merge with existing target data
resource.targets![targetIndex] = {
...resource.targets![targetIndex],
...finalTarget
};
}
);
}
}
}
);
}
}
// // Test example
// const testContainers: Container[] = [
// {
// id: "57e056cb0e3a",
// name: "nginx1",
// image: "nginxdemos/hello",
// state: "running",
// status: "Up 4 days",
// ports: [
// {
// privatePort: 80,
// publicPort: 8000,
// type: "tcp",
// ip: "0.0.0.0"
// }
// ],
// labels: {
// "resources.nginx.name": "nginx",
// "resources.nginx.full-domain": "nginx.example.com",
// "resources.nginx.protocol": "http",
// "resources.nginx.targets[0].enabled": "true"
// },
// created: 1756942725,
// networks: {
// owen_default: {
// networkId:
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
// }
// },
// hostname: "57e056cb0e3a"
// },
// {
// id: "58e056cb0e3b",
// name: "nginx2",
// image: "nginxdemos/hello",
// state: "running",
// status: "Up 4 days",
// ports: [
// {
// privatePort: 80,
// publicPort: 8001,
// type: "tcp",
// ip: "0.0.0.0"
// }
// ],
// labels: {
// "resources.nginx.name": "nginx",
// "resources.nginx.full-domain": "nginx.example.com",
// "resources.nginx.protocol": "http",
// "resources.nginx.targets[1].enabled": "true"
// },
// created: 1756942726,
// networks: {
// owen_default: {
// networkId:
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
// }
// },
// hostname: "58e056cb0e3b"
// },
// {
// id: "59e056cb0e3c",
// name: "api-server",
// image: "my-api:latest",
// state: "running",
// status: "Up 2 days",
// ports: [
// {
// privatePort: 3000,
// publicPort: 3000,
// type: "tcp",
// ip: "0.0.0.0"
// }
// ],
// labels: {
// "resources.api.name": "API Server",
// "resources.api.protocol": "http",
// "resources.api.targets[0].enabled": "true",
// "resources.api.targets[0].hostname": "custom-host",
// "resources.api.targets[0].port": "3001"
// },
// created: 1756942727,
// networks: {
// owen_default: {
// networkId:
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
// }
// },
// hostname: "59e056cb0e3c"
// },
// {
// id: "d0e29b08361c",
// name: "beautiful_wilson",
// image: "bolkedebruin/rdpgw:latest",
// state: "exited",
// status: "Exited (0) 4 hours ago",
// ports: null,
// labels: {},
// created: 1757359039,
// networks: {
// bridge: {
// networkId:
// "ea7f56dfc9cc476b8a3560b5b570d0fe8a6a2bc5e8343ab1ed37822086e89687"
// }
// },
// hostname: "d0e29b08361c"
// }
// ];
// // Test the function
// const result = processContainerLabels(testContainers);
// console.log("Processed result:");
// console.log(JSON.stringify(result, null, 2));