Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Container-Compose/Commands/ComposeUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
for port in ports {
let resolvedPort = resolveVariable(port, with: environmentVariables)
runCommandArgs.append("-p")
runCommandArgs.append("0.0.0.0:\(resolvedPort)")
runCommandArgs.append(composePortToRunArg(resolvedPort))
}
}

Expand Down
42 changes: 42 additions & 0 deletions Sources/Container-Compose/Helper Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,48 @@ public func deriveProjectName(cwd: String) -> String {
return projectName
}

/// Converts Docker Compose port specification into a container run -p format.
/// Handles various formats: "PORT", "HOST:PORT", "IP:HOST:PORT", and optional protocol.
/// - Parameter portSpec: The port specification string from docker-compose.yml.
/// - Returns: A properly formatted port binding for `container run -p`.
public func composePortToRunArg(_ portSpec: String) -> String {
// Check for protocol suffix (e.g., "/tcp" or "/udp")
var protocolSuffix = ""
var portBody = portSpec
if let slashRange = portSpec.range(of: "/", options: [.backwards]) {
let afterSlash = portSpec[slashRange.lowerBound...]
let protocolPart = String(afterSlash)
if protocolPart == "/tcp" || protocolPart == "/udp" {
protocolSuffix = protocolPart
portBody = String(portSpec[..<slashRange.lowerBound])
}
}

let components = portBody.split(separator: ":", maxSplits: 3).map(String.init)
switch components.count {
case 1:
let containerPort = components[0]
return "0.0.0.0:\(containerPort):\(containerPort)\(protocolSuffix)"
case 2:
let hostPart = components[0]
let containerPart = components[1]
let hasIPv4 = hostPart.contains(".")
let hasIPv6 = hostPart.contains(":") && hostPart.hasPrefix("[") && hostPart.hasSuffix("]")
if hasIPv4 || hasIPv6 {
return "\(hostPart):\(containerPart)\(protocolSuffix)"
} else {
return "0.0.0.0:\(hostPart):\(containerPart)\(protocolSuffix)"
}
case 3:
let ipPart = components[0]
let hostPart = components[1]
let containerPart = components[2]
return "\(ipPart):\(hostPart):\(containerPart)\(protocolSuffix)"
default:
return portSpec
}
}

extension String: @retroactive Error {}

/// A structure representing the result of a command-line process execution.
Expand Down
50 changes: 49 additions & 1 deletion Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,52 @@ struct HelperFunctionsTests {
#expect(projectName == "_devcontainers")
}

}
@Test("Compose port - simple container port")
func testPortSimple() throws {
let result = composePortToRunArg("3000")
#expect(result == "0.0.0.0:3000:3000")
}

@Test("Compose port - host:container same port")
func testPortHostContainerSame() throws {
let result = composePortToRunArg("3000:3000")
#expect(result == "0.0.0.0:3000:3000")
}

@Test("Compose port - host:container different ports")
func testPortHostContainerDifferent() throws {
let result = composePortToRunArg("8080:3000")
#expect(result == "0.0.0.0:8080:3000")
}

@Test("Compose port - explicit IP binding IPv4")
func testPortIPv4Binding() throws {
let result = composePortToRunArg("127.0.0.1:5432:5432")
#expect(result == "127.0.0.1:5432:5432")
}

@Test("Compose port - explicit IP binding IPv6")
func testPortIPv6Binding() throws {
let result = composePortToRunArg("[::1]:3000:3000")
#expect(result == "[::1]:3000:3000")
}

@Test("Compose port - with protocol tcp")
func testPortWithProtocolTCP() throws {
let result = composePortToRunArg("3000:3000/tcp")
#expect(result == "0.0.0.0:3000:3000/tcp")
}

@Test("Compose port - explicit IP with protocol")
func testPortIPv4WithProtocol() throws {
let result = composePortToRunArg("127.0.0.1:5432:5432/tcp")
#expect(result == "127.0.0.1:5432:5432/tcp")
}

@Test("Compose port - explicit IP already with 0.0.0.0")
func testPortZeroZeroZeroZero() throws {
let result = composePortToRunArg("0.0.0.0:3000:3000")
#expect(result == "0.0.0.0:3000:3000")
}

}