mirror of
https://github.com/apple/container.git
synced 2026-07-12 20:47:04 +00:00
Adds --publish flag for forwarding traffic to container ports. (#352)
This commit is contained in:
@@ -23,6 +23,8 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
public var image: ImageDescription
|
||||
/// External mounts to add to the container.
|
||||
public var mounts: [Filesystem] = []
|
||||
/// Ports to publish from container to host.
|
||||
public var publishedPorts: [PublishPort] = []
|
||||
/// Sockets to publish from container to host.
|
||||
public var publishedSockets: [PublishSocket] = []
|
||||
/// Key/Value labels for the container.
|
||||
@@ -50,6 +52,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
case id
|
||||
case image
|
||||
case mounts
|
||||
case publishedPorts
|
||||
case publishedSockets
|
||||
case labels
|
||||
case sysctls
|
||||
@@ -71,6 +74,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
image = try container.decode(ImageDescription.self, forKey: .image)
|
||||
mounts = try container.decodeIfPresent([Filesystem].self, forKey: .mounts) ?? []
|
||||
publishedPorts = try container.decodeIfPresent([PublishPort].self, forKey: .publishedPorts) ?? []
|
||||
publishedSockets = try container.decodeIfPresent([PublishSocket].self, forKey: .publishedSockets) ?? []
|
||||
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
|
||||
sysctls = try container.decodeIfPresent([String: String].self, forKey: .sysctls) ?? [:]
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// The network protocols available for port forwarding.
|
||||
public enum PublishProtocol: String, Sendable, Codable {
|
||||
case tcp = "tcp"
|
||||
case udp = "udp"
|
||||
|
||||
/// Initialize a protocol with to default value, `.tcp`.
|
||||
public init() {
|
||||
self = .tcp
|
||||
}
|
||||
|
||||
/// Initialize a protocol value from the provided string.
|
||||
public init?(_ value: String) {
|
||||
switch value.lowercased() {
|
||||
case "tcp": self = .tcp
|
||||
case "udp": self = .udp
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies internet port forwarding from host to container.
|
||||
public struct PublishPort: Sendable, Codable {
|
||||
/// The IP address of the proxy listener on the host
|
||||
public let hostAddress: String
|
||||
|
||||
/// The port number of the proxy listener on the host
|
||||
public let hostPort: Int
|
||||
|
||||
/// The port number of the container listener
|
||||
public let containerPort: Int
|
||||
|
||||
/// The network protocol for the proxy
|
||||
public let proto: PublishProtocol
|
||||
|
||||
/// Creates a new port forwarding specification.
|
||||
public init(hostAddress: String, hostPort: Int, containerPort: Int, proto: PublishProtocol) {
|
||||
self.hostAddress = hostAddress
|
||||
self.hostPort = hostPort
|
||||
self.containerPort = containerPort
|
||||
self.proto = proto
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public struct Flags {
|
||||
public struct Management: ParsableArguments {
|
||||
public init() {}
|
||||
|
||||
@Flag(name: [.customLong("detach"), .customShort("d")], help: "Run the container and detach from the process")
|
||||
@Flag(name: [.customLong("detach"), .short], help: "Run the container and detach from the process")
|
||||
public var detach = false
|
||||
|
||||
@Option(name: .customLong("entrypoint"), help: "Override the entrypoint of the image")
|
||||
@@ -93,6 +93,9 @@ public struct Flags {
|
||||
@Option(name: .customLong("mount"), help: "Add a mount to the container (type=<>,source=<>,target=<>,readonly)")
|
||||
public var mounts: [String] = []
|
||||
|
||||
@Option(name: [.customLong("publish"), .short], help: "Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])")
|
||||
public var publishPorts: [String] = []
|
||||
|
||||
@Option(name: .customLong("publish-socket"), help: "Publish a socket from container to host (format: host_path:container_path)")
|
||||
public var publishSockets: [String] = []
|
||||
|
||||
@@ -109,14 +112,14 @@ public struct Flags {
|
||||
public var os = "linux"
|
||||
|
||||
@Option(
|
||||
name: [.customLong("arch"), .customShort("a")], help: "Set arch if image can target multiple architectures")
|
||||
name: [.customLong("arch"), .short], help: "Set arch if image can target multiple architectures")
|
||||
public var arch: String = Arch.hostArchitecture().rawValue
|
||||
|
||||
@Option(name: [.customLong("volume"), .customShort("v")], help: "Bind mount a volume into the container")
|
||||
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
|
||||
public var volumes: [String] = []
|
||||
|
||||
@Option(
|
||||
name: [.customLong("kernel"), .customShort("k")], help: "Set a custom kernel path", completion: .file(),
|
||||
name: [.customLong("kernel"), .short], help: "Set a custom kernel path", completion: .file(),
|
||||
transform: { str in
|
||||
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
|
||||
})
|
||||
@@ -143,7 +146,7 @@ public struct Flags {
|
||||
@Option(name: .customLong("dns-option"), help: "DNS options")
|
||||
public var dnsOptions: [String] = []
|
||||
|
||||
@Option(name: [.customLong("label"), .customShort("l")], help: "Add a key=value label to the container")
|
||||
@Option(name: [.customLong("label"), .short], help: "Add a key=value label to the container")
|
||||
public var labels: [String] = []
|
||||
}
|
||||
|
||||
|
||||
@@ -397,12 +397,84 @@ public struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse --publish-socket arguments into PublishSocket objects
|
||||
// Format: "host_path:container_path" (e.g., "/tmp/docker.sock:/var/run/docker.sock")
|
||||
//
|
||||
// - Parameter rawPublishSockets: Array of socket specifications
|
||||
// - Returns: Array of PublishSocket objects
|
||||
// - Throws: ContainerizationError if parsing fails
|
||||
/// Parse --publish-port arguments into PublishPort objects
|
||||
/// The format of each argument is `[host-ip:]host-port:container-port[/protocol]`
|
||||
/// (e.g., "127.0.0.1:8080:80/tcp")
|
||||
///
|
||||
/// - Parameter rawPublishPorts: Array of port arguments
|
||||
/// - Returns: Array of PublishPort objects
|
||||
/// - Throws: ContainerizationError if parsing fails
|
||||
static func publishPorts(_ rawPublishPorts: [String]) throws -> [PublishPort] {
|
||||
var sockets: [PublishPort] = []
|
||||
|
||||
// Process each raw port string
|
||||
for socket in rawPublishPorts {
|
||||
let parsedSocket = try Parser.publishPort(socket)
|
||||
sockets.append(parsedSocket)
|
||||
}
|
||||
return sockets
|
||||
}
|
||||
|
||||
// Parse a single `--publish-port` argument into a `PublishPort`.
|
||||
private static func publishPort(_ portText: String) throws -> PublishPort {
|
||||
let protoSplit = portText.split(separator: "/")
|
||||
let proto: PublishProtocol
|
||||
let addressAndPortText: String
|
||||
switch protoSplit.count {
|
||||
case 1:
|
||||
addressAndPortText = String(protoSplit[0])
|
||||
proto = .tcp
|
||||
case 2:
|
||||
addressAndPortText = String(protoSplit[0])
|
||||
let protoText = String(protoSplit[1])
|
||||
guard let parsedProto = PublishProtocol(protoText) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid publish protocol: \(protoText)")
|
||||
}
|
||||
proto = parsedProto
|
||||
default:
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid publish value: \(portText)")
|
||||
}
|
||||
|
||||
let hostAddress: String
|
||||
let hostPortText: String
|
||||
let containerPortText: String
|
||||
let parts = addressAndPortText.split(separator: ":")
|
||||
switch parts.count {
|
||||
case 2:
|
||||
hostAddress = "0.0.0.0"
|
||||
hostPortText = String(parts[0])
|
||||
containerPortText = String(parts[1])
|
||||
case 3:
|
||||
hostAddress = String(parts[0])
|
||||
hostPortText = String(parts[1])
|
||||
containerPortText = String(parts[2])
|
||||
default:
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid publish address: \(portText)")
|
||||
}
|
||||
|
||||
guard let hostPort = Int(hostPortText) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)")
|
||||
}
|
||||
|
||||
guard let containerPort = Int(containerPortText) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
|
||||
}
|
||||
|
||||
return PublishPort(
|
||||
hostAddress: hostAddress,
|
||||
hostPort: hostPort,
|
||||
containerPort: containerPort,
|
||||
proto: proto
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse --publish-socket arguments into PublishSocket objects
|
||||
/// The format of each argument is `host_path:container_path`
|
||||
/// (e.g., "/tmp/docker.sock:/var/run/docker.sock")
|
||||
///
|
||||
/// - Parameter rawPublishSockets: Array of socket arguments
|
||||
/// - Returns: Array of PublishSocket objects
|
||||
/// - Throws: ContainerizationError if parsing fails or a path is invalid
|
||||
static func publishSockets(_ rawPublishSockets: [String]) throws -> [PublishSocket] {
|
||||
var sockets: [PublishSocket] = []
|
||||
|
||||
@@ -414,11 +486,10 @@ public struct Parser {
|
||||
return sockets
|
||||
}
|
||||
|
||||
// Parse a single --publish-socket argument and validate paths
|
||||
// Format: "host_path:container_path" -> PublishSocket
|
||||
private static func publishSocket(_ socket: String) throws -> PublishSocket {
|
||||
// Parse a single `--publish-socket`` argument into a `PublishSocket`.
|
||||
private static func publishSocket(_ socketText: String) throws -> PublishSocket {
|
||||
// Split by colon to two parts: [host_path, container_path]
|
||||
let parts = socket.split(separator: ":")
|
||||
let parts = socketText.split(separator: ":")
|
||||
|
||||
switch parts.count {
|
||||
case 2:
|
||||
@@ -483,7 +554,7 @@ public struct Parser {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message:
|
||||
"invalid publish-socket format \(socket). Expected: host_path:container_path")
|
||||
"invalid publish-socket format \(socketText). Expected: host_path:container_path")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,8 @@ public struct Utility {
|
||||
|
||||
config.labels = try Parser.labels(management.labels)
|
||||
|
||||
config.publishedPorts = try Parser.publishPorts(management.publishPorts)
|
||||
|
||||
// Parse --publish-socket arguments and add to container configuration
|
||||
// to enable socket forwarding from container to host.
|
||||
config.publishedSockets = try Parser.publishSockets(management.publishSockets)
|
||||
|
||||
@@ -26,6 +26,9 @@ import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
import SocketForwarder
|
||||
|
||||
import struct ContainerizationOCI.Mount
|
||||
import struct ContainerizationOCI.Process
|
||||
@@ -36,11 +39,13 @@ public actor SandboxService {
|
||||
private let interfaceStrategy: InterfaceStrategy
|
||||
private var container: ContainerInfo?
|
||||
private let monitor: ExitMonitor
|
||||
private let eventLoopGroup: MultiThreadedEventLoopGroup
|
||||
private var waiters: [String: [CheckedContinuation<Int32, Never>]] = [:]
|
||||
private let lock: AsyncLock = AsyncLock()
|
||||
private let log: Logging.Logger
|
||||
private var state: State = .created
|
||||
private var processes: [String: ProcessInfo] = [:]
|
||||
private var socketForwarders: [SocketForwarderResult] = []
|
||||
|
||||
/// Create an instance with a bundle that describes the container.
|
||||
///
|
||||
@@ -54,6 +59,7 @@ public actor SandboxService {
|
||||
self.interfaceStrategy = interfaceStrategy
|
||||
self.log = log
|
||||
self.monitor = ExitMonitor(log: log)
|
||||
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
||||
}
|
||||
|
||||
/// Start the VM and the guest agent process for a container.
|
||||
@@ -142,6 +148,11 @@ public actor SandboxService {
|
||||
do {
|
||||
try await container.create()
|
||||
try await self.monitor.registerProcess(id: config.id, onExit: self.onContainerExit)
|
||||
if !container.interfaces.isEmpty {
|
||||
let firstCidr = try CIDRAddress(container.interfaces[0].address)
|
||||
let ipAddress = firstCidr.address.description
|
||||
try await self.startSocketForwarders(containerIpAddress: ipAddress, publishedPorts: config.publishedPorts)
|
||||
}
|
||||
await self.setState(.booted)
|
||||
} catch {
|
||||
do {
|
||||
@@ -274,6 +285,57 @@ public actor SandboxService {
|
||||
try await self.monitor.track(id: id, waitingOn: waitFunc)
|
||||
}
|
||||
|
||||
private func startSocketForwarders(containerIpAddress: String, publishedPorts: [PublishPort]) async throws {
|
||||
var forwarders: [SocketForwarderResult] = []
|
||||
try await withThrowingTaskGroup(of: SocketForwarderResult.self) { group in
|
||||
for publishedPort in publishedPorts {
|
||||
let proxyAddress = try SocketAddress(ipAddress: publishedPort.hostAddress, port: Int(publishedPort.hostPort))
|
||||
let serverAddress = try SocketAddress(ipAddress: containerIpAddress, port: Int(publishedPort.containerPort))
|
||||
log.info(
|
||||
"creating forwarder for",
|
||||
metadata: [
|
||||
"proxy": "\(proxyAddress)",
|
||||
"server": "\(serverAddress)",
|
||||
"protocol": "\(publishedPort.proto)",
|
||||
])
|
||||
group.addTask {
|
||||
let forwarder: SocketForwarder
|
||||
switch publishedPort.proto {
|
||||
case .tcp:
|
||||
forwarder = try TCPForwarder(
|
||||
proxyAddress: proxyAddress,
|
||||
serverAddress: serverAddress,
|
||||
eventLoopGroup: self.eventLoopGroup,
|
||||
log: self.log
|
||||
)
|
||||
case .udp:
|
||||
forwarder = try UDPForwarder(
|
||||
proxyAddress: proxyAddress,
|
||||
serverAddress: serverAddress,
|
||||
eventLoopGroup: self.eventLoopGroup,
|
||||
log: self.log
|
||||
)
|
||||
}
|
||||
return try await forwarder.run().get()
|
||||
}
|
||||
}
|
||||
for try await result in group {
|
||||
forwarders.append(result)
|
||||
}
|
||||
}
|
||||
|
||||
self.socketForwarders = forwarders
|
||||
}
|
||||
|
||||
private func stopSocketForwarders() async {
|
||||
log.info("closing forwarders")
|
||||
for forwarder in self.socketForwarders {
|
||||
forwarder.close()
|
||||
try? await forwarder.wait()
|
||||
}
|
||||
log.info("closed forwarders")
|
||||
}
|
||||
|
||||
/// Create a process inside the virtual machine for the container.
|
||||
///
|
||||
/// Use this procedure to run ad hoc processes in the virtual
|
||||
@@ -776,6 +838,7 @@ public actor SandboxService {
|
||||
|
||||
private func cleanupContainer() async throws {
|
||||
// Give back our lovely IP(s)
|
||||
await self.stopSocketForwarders()
|
||||
let containerInfo = try self.getContainer()
|
||||
for attachment in containerInfo.attachments {
|
||||
let client = NetworkClient(id: attachment.network)
|
||||
|
||||
@@ -80,20 +80,4 @@ class TestCLINetwork: CLITest {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func getClient() -> HTTPClient {
|
||||
var httpConfiguration = HTTPClient.Configuration()
|
||||
let proxyConfig: HTTPClient.Configuration.Proxy? = {
|
||||
let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"]
|
||||
guard let proxyEnv else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: proxyEnv), let host = url.host(), let port = url.port else {
|
||||
return nil
|
||||
}
|
||||
return .server(host: host, port: port)
|
||||
}()
|
||||
httpConfiguration.proxy = proxyConfig
|
||||
return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
//
|
||||
|
||||
import AsyncHTTPClient
|
||||
import ContainerClient
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Testing
|
||||
@@ -426,4 +428,48 @@ class TestCLIRunCommand: CLITest {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testForwardTCP() async throws {
|
||||
let retries = 10
|
||||
let retryDelaySeconds = Int64(3)
|
||||
do {
|
||||
let name = Test.current!.name.trimmingCharacters(in: ["(", ")"])
|
||||
let proxyIp = "127.0.0.1"
|
||||
let proxyPort = UInt16.random(in: 50000..<55000)
|
||||
let serverPort = UInt16.random(in: 55000..<60000)
|
||||
try doLongRun(
|
||||
name: name,
|
||||
image: "docker.io/library/python:alpine",
|
||||
args: ["--publish", "\(proxyIp):\(proxyPort):\(serverPort)/tcp"],
|
||||
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(serverPort)"])
|
||||
defer {
|
||||
try? doStop(name: name)
|
||||
}
|
||||
|
||||
let url = "http://\(proxyIp):\(proxyPort)"
|
||||
var request = HTTPClientRequest(url: url)
|
||||
request.method = .GET
|
||||
let config = HTTPClient.Configuration(proxy: nil)
|
||||
let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
|
||||
defer { _ = client.shutdown() }
|
||||
var retriesRemaining = retries
|
||||
var success = false
|
||||
while !success && retriesRemaining > 0 {
|
||||
do {
|
||||
let response = try await client.execute(request, timeout: .seconds(retryDelaySeconds))
|
||||
try #require(response.status == .ok)
|
||||
success = true
|
||||
} catch {
|
||||
print("request to \(url) failed, error \(error)")
|
||||
try await Task.sleep(for: .seconds(retryDelaySeconds))
|
||||
}
|
||||
retriesRemaining -= 1
|
||||
}
|
||||
#expect(success, "Request to \(url) failed after \(retries - retriesRemaining) retries")
|
||||
try doStop(name: name)
|
||||
} catch {
|
||||
Issue.record("failed to run container \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
//
|
||||
|
||||
import AsyncHTTPClient
|
||||
import ContainerClient
|
||||
import ContainerNetworkService
|
||||
import Containerization
|
||||
@@ -382,4 +383,20 @@ class CLITest {
|
||||
throw CLIError.executionFailed("command failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getClient() -> HTTPClient {
|
||||
var httpConfiguration = HTTPClient.Configuration()
|
||||
let proxyConfig: HTTPClient.Configuration.Proxy? = {
|
||||
let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"]
|
||||
guard let proxyEnv else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: proxyEnv), let host = url.host(), let port = url.port else {
|
||||
return nil
|
||||
}
|
||||
return .server(host: host, port: port)
|
||||
}()
|
||||
httpConfiguration.proxy = proxyConfig
|
||||
return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
//
|
||||
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerClient
|
||||
|
||||
struct ParserTest {
|
||||
@Test
|
||||
func testPublishPortParserTcp() throws {
|
||||
let result = try Parser.publishPorts(["127.0.0.1:8080:8000/tcp"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "127.0.0.1")
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortParserUdp() throws {
|
||||
let result = try Parser.publishPorts(["192.168.32.36:8000:8080/UDP"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "192.168.32.36")
|
||||
#expect(result[0].hostPort == UInt16(8000))
|
||||
#expect(result[0].containerPort == UInt16(8080))
|
||||
#expect(result[0].proto == .udp)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortNoHostAddress() throws {
|
||||
let result = try Parser.publishPorts(["8080:8000/tcp"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "0.0.0.0")
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortNoProtocol() throws {
|
||||
let result = try Parser.publishPorts(["8080:8000"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "0.0.0.0")
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidProtocol() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts(["8080:8000/sctp"])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish protocol")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidValue() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts([""])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish value")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidAddress() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts(["1234"])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish address")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidHostPort() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts(["foo:1234"])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish host port")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidContainerPort() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts(["1234:foo"])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish container port")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,57 @@ Use the `list` command with the `--format` option to display information for all
|
||||
]
|
||||
</pre>
|
||||
|
||||
## Forward traffic from `localhost` to your container
|
||||
|
||||
Use the `--publish` option to forward TCP or UDP traffic from your loopback IP to the container you run. The option value has the form `[host-ip:]host-port:container-port[/protocol]`, where protocol may be `tcp` or `udp`, case insensitive.
|
||||
|
||||
If your container attaches to multiple networks, the ports you publish forward to the IP address of the interface attached to the first network.
|
||||
|
||||
To forward requests from `localhost:8080` to a Python webserver on container port 8000, run:
|
||||
|
||||
```bash
|
||||
container run -d --rm -p 127.0.0.1:8080:8000 python:slim python3 -m http.server --bind 0.0.0.0 8000
|
||||
```
|
||||
|
||||
A `curl` to `localhost:8000` outputs:
|
||||
|
||||
```console
|
||||
% curl http://localhost:8080
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Directory listing for /</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Directory listing for /</h1>
|
||||
<hr>
|
||||
<ul>
|
||||
<li><a href="bin/">bin@</a></li>
|
||||
<li><a href="boot/">boot/</a></li>
|
||||
<li><a href="dev/">dev/</a></li>
|
||||
<li><a href="etc/">etc/</a></li>
|
||||
<li><a href="home/">home/</a></li>
|
||||
<li><a href="lib/">lib@</a></li>
|
||||
<li><a href="lost%2Bfound/">lost+found/</a></li>
|
||||
<li><a href="media/">media/</a></li>
|
||||
<li><a href="mnt/">mnt/</a></li>
|
||||
<li><a href="opt/">opt/</a></li>
|
||||
<li><a href="proc/">proc/</a></li>
|
||||
<li><a href="root/">root/</a></li>
|
||||
<li><a href="run/">run/</a></li>
|
||||
<li><a href="sbin/">sbin@</a></li>
|
||||
<li><a href="srv/">srv/</a></li>
|
||||
<li><a href="sys/">sys/</a></li>
|
||||
<li><a href="tmp/">tmp/</a></li>
|
||||
<li><a href="usr/">usr/</a></li>
|
||||
<li><a href="var/">var/</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Create and use a separate isolated network
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
Reference in New Issue
Block a user