Adds --publish flag for forwarding traffic to container ports. (#352)

This commit is contained in:
J Logan
2025-07-23 00:28:41 -07:00
committed by GitHub
parent c0d1f8fb11
commit eea8cb6709
11 changed files with 456 additions and 32 deletions
@@ -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
}
}
+8 -5
View File
@@ -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] = []
}
+82 -11
View File
@@ -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")
}
}
}
+2
View File
@@ -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)