iop
Version:
Ship Docker Anywhere
252 lines • 11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PortChecker = void 0;
exports.parsePortMappings = parsePortMappings;
class PortChecker {
constructor(sshClient, dockerClient, serverHostname, verbose = false) {
this.sshClient = sshClient;
this.dockerClient = dockerClient;
this.serverHostname = serverHostname;
this.verbose = verbose;
}
/**
* Check what ports are currently in use on the server
*/
async getPortUsage() {
const portUsage = [];
try {
// Get Docker container port mappings
const dockerPorts = await this.getDockerPortUsage();
portUsage.push(...dockerPorts);
// Get system process port usage (excluding ports already used by Docker)
const dockerPortNumbers = new Set(dockerPorts.map(dp => dp.port));
const systemPorts = await this.getSystemPortUsage(dockerPortNumbers);
portUsage.push(...systemPorts);
return portUsage;
}
catch (error) {
if (this.verbose) {
console.warn(`[${this.serverHostname}] Failed to get port usage: ${error}`);
}
return [];
}
}
/**
* Get port usage from Docker containers
*/
async getDockerPortUsage() {
const portUsage = [];
try {
// Get all running containers with port mappings
const containerOutput = await this.sshClient.exec(`docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}" --no-trunc`);
const lines = containerOutput.split("\n").slice(1); // Skip header
for (const line of lines) {
if (!line.trim())
continue;
const parts = line.split("\t");
if (parts.length < 3)
continue;
const [containerName, image, ports] = parts;
// Parse port mappings (e.g., "0.0.0.0:5432->5432/tcp, 0.0.0.0:8080->8080/tcp")
if (ports && ports !== "") {
const portMappings = ports.split(",");
for (const mapping of portMappings) {
const match = mapping
.trim()
.match(/(?:(?:0\.0\.0\.0:)|(?:\[::\]:))?(\d+)->(\d+)\/(tcp|udp)/);
if (match) {
const [, hostPort, containerPort, protocol] = match;
portUsage.push({
port: parseInt(hostPort),
protocol: protocol,
containerName: containerName.trim(),
containerImage: image.trim(),
isDockerContainer: true,
});
}
}
}
}
}
catch (error) {
if (this.verbose) {
console.warn(`[${this.serverHostname}] Failed to get Docker port usage: ${error}`);
}
}
return portUsage;
}
/**
* Get port usage from system processes (non-Docker)
*/
async getSystemPortUsage(excludeDockerPorts = new Set()) {
const portUsage = [];
try {
// Use netstat to get listening ports
const netstatOutput = await this.sshClient.exec(`netstat -tlnp 2>/dev/null | grep LISTEN || ss -tlnp | grep LISTEN`);
const lines = netstatOutput.split("\n");
for (const line of lines) {
if (!line.includes("LISTEN"))
continue;
// Parse different netstat/ss output formats
const parts = line.trim().split(/\s+/);
// Try to extract port from address (e.g., "0.0.0.0:5432", ":::80")
let portMatch = null;
for (const part of parts) {
portMatch = part.match(/:(\d+)$/);
if (portMatch)
break;
}
if (portMatch) {
const port = parseInt(portMatch[1]);
// Skip ports that are already handled by Docker containers
if (excludeDockerPorts.has(port)) {
if (this.verbose) {
console.log(`[${this.serverHostname}] Skipping port ${port} - already handled by Docker`);
}
continue;
}
// Try to extract process info (last part usually contains PID/process)
const lastPart = parts[parts.length - 1];
const processMatch = lastPart.match(/(\d+)\/(.*)/);
let process = undefined;
let pid = undefined;
if (processMatch) {
pid = parseInt(processMatch[1]);
process = processMatch[2];
}
// Skip if this looks like a Docker process (we already got those)
if (process &&
(process.includes("docker") || process.includes("containerd"))) {
continue;
}
portUsage.push({
port,
protocol: line.includes("tcp") ? "tcp" : "udp",
process,
pid,
isDockerContainer: false,
});
}
}
}
catch (error) {
if (this.verbose) {
console.warn(`[${this.serverHostname}] Failed to get system port usage: ${error}`);
}
}
return portUsage;
}
/**
* Check for port conflicts with planned deployments
*/
async checkPortConflicts(plannedPorts, projectName) {
const currentUsage = await this.getPortUsage();
const conflicts = [];
for (const planned of plannedPorts) {
const conflict = currentUsage.find((usage) => usage.port === planned.hostPort &&
usage.protocol === (planned.protocol || "tcp"));
if (conflict) {
// If this is a Docker container from the same project, it's not a conflict
// since we'll replace it during deployment
if (conflict.isDockerContainer && projectName && conflict.containerName) {
const isOwnProjectContainer = conflict.containerName.startsWith(`${projectName}-`);
if (isOwnProjectContainer) {
if (this.verbose) {
console.log(`[${this.serverHostname}] Port ${planned.hostPort} used by own project container ${conflict.containerName}, will be replaced`);
}
continue; // Skip this conflict since we'll replace the container
}
}
conflicts.push({
port: planned.hostPort,
hostPort: planned.hostPort,
containerPort: planned.containerPort,
requestedBy: planned.requestedBy,
conflictsWith: conflict,
serverHostname: this.serverHostname,
});
}
}
return conflicts;
}
/**
* Generate suggestions for resolving port conflicts
*/
generateConflictSuggestions(conflicts) {
const suggestions = [];
if (conflicts.length === 0) {
return suggestions;
}
suggestions.push("Port conflicts detected! Here are some solutions:");
suggestions.push("");
for (const conflict of conflicts) {
suggestions.push(`✗ Port ${conflict.port} conflict for ${conflict.requestedBy}:`);
if (conflict.conflictsWith.isDockerContainer) {
suggestions.push(` Already used by Docker container: ${conflict.conflictsWith.containerName} (${conflict.conflictsWith.containerImage})`);
}
else {
suggestions.push(` Already used by system process: ${conflict.conflictsWith.process || "unknown"} (PID: ${conflict.conflictsWith.pid || "unknown"})`);
}
suggestions.push("");
suggestions.push(" Solutions:");
suggestions.push(` 1. Use a different host port: "${conflict.port + 1000}:${conflict.containerPort}"`);
suggestions.push(` 2. Remove port mapping entirely if external access isn't needed`);
suggestions.push(` 3. Stop the conflicting service if it's not needed`);
suggestions.push(` 4. Deploy to a different server`);
suggestions.push("");
}
suggestions.push("Examples of fixing port conflicts:");
suggestions.push("");
suggestions.push("Option 1 - Use different host ports:");
suggestions.push("services:");
suggestions.push(" postgres:");
suggestions.push(" ports:");
suggestions.push(` - "${conflicts[0]?.port + 1000 || 6432}:5432" # Changed from ${conflicts[0]?.port || 5432}:5432`);
suggestions.push("");
suggestions.push("Option 2 - Remove external port access (recommended):");
suggestions.push("services:");
suggestions.push(" postgres:");
suggestions.push(" # ports: [...] # Remove this line");
suggestions.push(" # Access via internal network only");
suggestions.push("");
return suggestions;
}
}
exports.PortChecker = PortChecker;
/**
* Extract port mappings from Docker port specification
*/
function parsePortMappings(ports) {
const mappings = [];
for (const port of ports) {
// Handle different port formats:
// "5432:5432"
// "5432:5432/tcp"
// "127.0.0.1:5432:5432"
// "5432"
let match = port.match(/^(?:\d+\.\d+\.\d+\.\d+:)?(\d+):(\d+)(?:\/(tcp|udp))?$/);
if (match) {
const [, hostPort, containerPort, protocol = "tcp"] = match;
mappings.push({
hostPort: parseInt(hostPort),
containerPort: parseInt(containerPort),
protocol: protocol,
});
}
else {
// Handle single port (maps to same port)
match = port.match(/^(\d+)(?:\/(tcp|udp))?$/);
if (match) {
const [, portNum, protocol = "tcp"] = match;
const portNumber = parseInt(portNum);
mappings.push({
hostPort: portNumber,
containerPort: portNumber,
protocol: protocol,
});
}
}
}
return mappings;
}
//# sourceMappingURL=port-checker.js.map