import { PortMap } from 'docker-types/generated/1.41'; import _ from 'lodash'; import { Protocol, Values } from './PortsMappingField'; export type Range = { start: number; end: number; }; type StringPortBinding = { hostPort: string; protocol: Protocol; containerPort: number; }; type NumericPortBinding = { hostPort: number; protocol: Protocol; containerPort: number; }; type RangePortBinding = { hostPort: Range; protocol: Protocol; containerPort: Range; }; export function toViewModel(portBindings: PortMap): Values { const parsedPorts = parsePorts(portBindings); const sortedPorts = sortPorts(parsedPorts); return [ ...sortedPorts.rangePorts.map((port) => ({ ...port, containerPort: String(port.containerPort), })), ...combinePorts(sortedPorts.nonRangePorts), ]; function isProtocol(value: string): value is Protocol { return value === 'tcp' || value === 'udp'; } function parsePorts( portBindings: PortMap ): Array { return Object.entries(portBindings).flatMap(([key, bindings]) => { const [containerPort, protocol] = key.split('/'); if (!isProtocol(protocol)) { throw new Error(`Invalid protocol: ${protocol}`); } if (!bindings) { return []; } const containerPortNumber = parseInt(containerPort, 10); if (Number.isNaN(containerPortNumber)) { throw new Error(`Invalid container port: ${containerPort}`); } return bindings.map((binding) => { if (binding.HostPort?.includes('-')) { return { hostPort: binding.HostPort, protocol, containerPort: containerPortNumber, }; } return { hostPort: parseInt(binding.HostPort || '0', 10), protocol, containerPort: containerPortNumber, }; }); }); } function sortPorts(ports: Array) { const rangePorts = ports.filter(isStringPortBinding); const nonRangePorts = ports.filter(isNumericPortBinding); return { rangePorts, nonRangePorts: _.sortBy(nonRangePorts, [ 'containerPort', 'hostPort', 'protocol', ]), }; } function combinePorts(ports: Array) { return ports .reduce((acc, port) => { const lastPort = acc[acc.length - 1]; if ( lastPort && lastPort.containerPort.end === port.containerPort - 1 && lastPort.hostPort.end === port.hostPort - 1 && lastPort.protocol === port.protocol ) { lastPort.containerPort.end = port.containerPort; lastPort.hostPort.end = port.hostPort; return acc; } return [ ...acc, { hostPort: { start: port.hostPort, end: port.hostPort, }, containerPort: { start: port.containerPort, end: port.containerPort, }, protocol: port.protocol, }, ]; }, [] as Array) .map(({ protocol, containerPort, hostPort }) => ({ hostPort: getRange(hostPort.start, hostPort.end), containerPort: getRange(containerPort.start, containerPort.end), protocol, })); function getRange(start: number, end: number): string { if (start === end) { if (start === 0) { return ''; } return start.toString(); } return `${start}-${end}`; } } } function isNumericPortBinding( port: StringPortBinding | NumericPortBinding ): port is NumericPortBinding { return port.hostPort !== 'string'; } function isStringPortBinding( port: StringPortBinding | NumericPortBinding ): port is StringPortBinding { return port.hostPort === 'string'; }