mirror of
https://github.com/apple/container.git
synced 2026-09-21 15:15:36 +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)
|
||||
|
||||
Reference in New Issue
Block a user