mirror of
https://github.com/apple/container.git
synced 2026-08-24 02:24:19 -05:00
Remove XPC compatibility code, simplify network model. (#1616)
- Refactor network model types: replace `NetworkState` enum and phase-based NetworkStatus with a flat `NetworkStatus` struct. - Simplify API server ↔ plugin protocol: plugin `status()` returns runtime status only, API server owns configuration. - `NetworksService` `list()`/`create()` now return `NetworkResource` directly. - Remove lifecycle phase checks and state machine guards throughout CLI and API server. - `variant` is plugin-specific, it's not a required property. This PR replaces `NetworkPluginInfo` with a `plugin` name property on `NetworkConfiguration` and an `options` list similar to that for volumes. - Moved `variant` to the option list.
This commit is contained in:
@@ -339,7 +339,7 @@ extension APIServer {
|
||||
ipv4Subnet: containerSystemConfig.network.subnet,
|
||||
ipv6Subnet: containerSystemConfig.network.subnetv6,
|
||||
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
|
||||
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")
|
||||
plugin: "container-network-vmnet"
|
||||
)
|
||||
_ = try await service.create(configuration: config)
|
||||
}
|
||||
|
||||
@@ -266,9 +266,6 @@ extension Application {
|
||||
guard let defaultNetwork = try await networkClient.builtin else {
|
||||
throw ContainerizationError(.invalidState, message: "default network is not present")
|
||||
}
|
||||
guard defaultNetwork.status.phase == "running" else {
|
||||
throw ContainerizationError(.invalidState, message: "default network is not running")
|
||||
}
|
||||
config.networks = [
|
||||
AttachmentConfiguration(network: defaultNetwork.id, options: AttachmentOptions(hostname: Builder.builderContainerId))
|
||||
]
|
||||
|
||||
@@ -28,11 +28,17 @@ extension Application {
|
||||
commandName: "create",
|
||||
abstract: "Create a new network")
|
||||
|
||||
@Flag(name: .customLong("internal"), help: "Restrict to host-only network")
|
||||
var hostOnly: Bool = false
|
||||
|
||||
@Option(name: .customLong("label"), help: "Set metadata for a network")
|
||||
var labels: [String] = []
|
||||
|
||||
@Flag(name: .customLong("internal"), help: "Restrict to host-only network")
|
||||
var hostOnly: Bool = false
|
||||
@Option(name: .customLong("option"), help: "Set a plugin-specific option (key=value)")
|
||||
var options: [String] = []
|
||||
|
||||
@Option(name: .long, help: "Set the plugin to use to create this network.")
|
||||
var plugin: String = "container-network-vmnet"
|
||||
|
||||
@Option(
|
||||
name: .customLong("subnet"), help: "Set subnet for a network",
|
||||
@@ -48,12 +54,6 @@ extension Application {
|
||||
})
|
||||
var ipv6Subnet: CIDRv6? = nil
|
||||
|
||||
@Option(name: .long, help: "Set the plugin to use to create this network.")
|
||||
var plugin: String = "container-network-vmnet"
|
||||
|
||||
@Option(name: .long, help: "Set the variant of the network plugin to use.")
|
||||
var pluginVariant: String?
|
||||
|
||||
@OptionGroup
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
@@ -64,6 +64,7 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
let parsedLabels = try ResourceLabels(Utility.parseKeyValuePairs(labels))
|
||||
let parsedOptions = Utility.parseKeyValuePairs(options)
|
||||
let mode: NetworkMode = hostOnly ? .hostOnly : .nat
|
||||
let config = try NetworkConfiguration(
|
||||
id: self.name,
|
||||
@@ -71,7 +72,8 @@ extension Application {
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
ipv6Subnet: ipv6Subnet,
|
||||
labels: parsedLabels,
|
||||
pluginInfo: NetworkPluginInfo(plugin: self.plugin, variant: self.pluginVariant)
|
||||
plugin: self.plugin,
|
||||
options: parsedOptions
|
||||
)
|
||||
let networkClient = NetworkClient()
|
||||
let network = try await networkClient.create(configuration: config)
|
||||
|
||||
@@ -18,11 +18,11 @@ import ContainerResource
|
||||
|
||||
extension NetworkResource: ListDisplayable {
|
||||
public static var tableHeader: [String] {
|
||||
["NETWORK", "STATE", "SUBNET"]
|
||||
["NETWORK", "SUBNET"]
|
||||
}
|
||||
|
||||
public var tableRow: [String] {
|
||||
[id, status.phase, status.ipv4Subnet?.description ?? "none"]
|
||||
[id, status.ipv4Subnet.description]
|
||||
}
|
||||
|
||||
public var quietValue: String {
|
||||
|
||||
@@ -18,16 +18,6 @@ import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
|
||||
public struct NetworkPluginInfo: Codable, Sendable, Hashable {
|
||||
public let plugin: String
|
||||
public let variant: String?
|
||||
|
||||
public init(plugin: String, variant: String? = nil) {
|
||||
self.plugin = plugin
|
||||
self.variant = variant
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration parameters for network creation.
|
||||
public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
/// A unique identifier for the network
|
||||
@@ -49,10 +39,11 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
/// Resource labels should not be mutated, except while building a network configurations.
|
||||
public let labels: ResourceLabels
|
||||
|
||||
/// Details about the network plugin that manages this network.
|
||||
/// FIXME: This field only needs to be optional while we wait for the field
|
||||
/// to be proliferated to most users when they update container.
|
||||
public let pluginInfo: NetworkPluginInfo?
|
||||
/// The network plugin that manages this network.
|
||||
public let plugin: String
|
||||
|
||||
/// Plugin-specific options for this network.
|
||||
public let options: [String: String]
|
||||
|
||||
/// Creates a network configuration
|
||||
public init(
|
||||
@@ -61,7 +52,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
ipv4Subnet: CIDRv4? = nil,
|
||||
ipv6Subnet: CIDRv6? = nil,
|
||||
labels: ResourceLabels = .init(),
|
||||
pluginInfo: NetworkPluginInfo?
|
||||
plugin: String,
|
||||
options: [String: String] = [:]
|
||||
) throws {
|
||||
self.id = id
|
||||
self.creationDate = Date()
|
||||
@@ -69,7 +61,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
self.ipv4Subnet = ipv4Subnet
|
||||
self.ipv6Subnet = ipv6Subnet
|
||||
self.labels = labels
|
||||
self.pluginInfo = pluginInfo
|
||||
self.plugin = plugin
|
||||
self.options = options
|
||||
try validate()
|
||||
}
|
||||
|
||||
@@ -80,8 +73,10 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
case ipv4Subnet
|
||||
case ipv6Subnet
|
||||
case labels
|
||||
case plugin
|
||||
case options
|
||||
// TODO: retain for deserialization compatibility, remove in next major version
|
||||
case pluginInfo
|
||||
// TODO: retain for deserialization compatibility for now, remove later
|
||||
case subnet
|
||||
}
|
||||
|
||||
@@ -101,7 +96,22 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
.map { try CIDRv6($0) }
|
||||
let decodedLabels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
|
||||
labels = try .init(decodedLabels)
|
||||
pluginInfo = try container.decodeIfPresent(NetworkPluginInfo.self, forKey: .pluginInfo)
|
||||
|
||||
if let plugin = try container.decodeIfPresent(String.self, forKey: .plugin) {
|
||||
self.plugin = plugin
|
||||
self.options = try container.decodeIfPresent([String: String].self, forKey: .options) ?? [:]
|
||||
} else if let legacy = try container.decodeIfPresent(_LegacyPluginInfo.self, forKey: .pluginInfo) {
|
||||
// - Deprecated: As of 1.0.0. Use ``plugin`` and ``options`` instead.
|
||||
// - Note: Will be removed in a later release.
|
||||
self.plugin = legacy.plugin
|
||||
var opts: [String: String] = [:]
|
||||
if let variant = legacy.variant { opts["variant"] = variant }
|
||||
self.options = opts
|
||||
} else {
|
||||
self.plugin = "container-network-vmnet"
|
||||
self.options = [:]
|
||||
}
|
||||
|
||||
try validate()
|
||||
}
|
||||
|
||||
@@ -115,7 +125,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
try container.encodeIfPresent(ipv4Subnet, forKey: .ipv4Subnet)
|
||||
try container.encodeIfPresent(ipv6Subnet, forKey: .ipv6Subnet)
|
||||
try container.encode(labels, forKey: .labels)
|
||||
try container.encodeIfPresent(pluginInfo, forKey: .pluginInfo)
|
||||
try container.encode(plugin, forKey: .plugin)
|
||||
try container.encode(options, forKey: .options)
|
||||
}
|
||||
|
||||
private func validate() throws {
|
||||
@@ -124,3 +135,9 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode helper for stored configurations that used the old `pluginInfo` key.
|
||||
private struct _LegacyPluginInfo: Codable {
|
||||
let plugin: String
|
||||
let variant: String?
|
||||
}
|
||||
|
||||
@@ -20,34 +20,32 @@ import Foundation
|
||||
/// A network resource, representing a configured virtual network and its runtime status.
|
||||
///
|
||||
/// `NetworkResource` conforms to `ManagedResource` and separates the network's
|
||||
/// intrinsic configuration from its ephemeral runtime status — following the same
|
||||
/// config/status split used by Kubernetes and Docker. `config` is persisted;
|
||||
/// `status` reflects what the network plugin reports at runtime.
|
||||
/// intrinsic configuration from its runtime status — following the same config/status
|
||||
/// split used by Kubernetes and Docker. `configuration` is persisted; `status` reflects
|
||||
/// what the network plugin reports at runtime.
|
||||
///
|
||||
/// JSON encoding produces four top-level keys: `id`, `state` (the lifecycle label:
|
||||
/// `"created"` or `"running"`), `configuration` (the persistent config), and `status`
|
||||
/// (runtime address properties, `null` when `state` is `"created"`).
|
||||
/// JSON encoding produces three top-level keys: `id`, `configuration` (the persistent
|
||||
/// config), and `status` (runtime address properties assigned by the network plugin).
|
||||
public struct NetworkResource: ManagedResource {
|
||||
/// The network's configuration — its persistent, intrinsic properties.
|
||||
public let config: NetworkConfiguration
|
||||
public let configuration: NetworkConfiguration
|
||||
|
||||
/// The network's current status, including lifecycle phase and any
|
||||
/// runtime-allocated address properties.
|
||||
/// The network's runtime status — the addresses assigned by the network plugin.
|
||||
public let status: NetworkStatus
|
||||
|
||||
// MARK: ManagedResource
|
||||
|
||||
/// The unique identifier for this network. Identical to ``config/id``.
|
||||
public var id: String { config.id }
|
||||
/// The unique identifier for this network. Identical to ``configuration/id``.
|
||||
public var id: String { configuration.id }
|
||||
|
||||
/// The user-assigned name for this network. For networks, name and ID are the same.
|
||||
public var name: String { config.id }
|
||||
public var name: String { configuration.id }
|
||||
|
||||
/// The time at which this network was created.
|
||||
public var creationDate: Date { config.creationDate }
|
||||
public var creationDate: Date { configuration.creationDate }
|
||||
|
||||
/// Key-value labels for this network.
|
||||
public var labels: ResourceLabels { config.labels }
|
||||
public var labels: ResourceLabels { configuration.labels }
|
||||
|
||||
/// Returns `true` for a system-managed network that cannot be deleted by the user.
|
||||
public var isBuiltin: Bool { labels.isBuiltin }
|
||||
@@ -66,29 +64,11 @@ public struct NetworkResource: ManagedResource {
|
||||
/// Creates a network resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - config: The network's intrinsic configuration.
|
||||
/// - networkStatus: The plugin-reported runtime status, or `nil` if the
|
||||
/// network is not yet running.
|
||||
public init(config: NetworkConfiguration, networkStatus: NetworkPluginStatus? = nil) {
|
||||
self.config = config
|
||||
self.status = networkStatus.map { NetworkStatus(running: $0) } ?? .created
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conversion from NetworkState
|
||||
|
||||
extension NetworkResource {
|
||||
/// Creates a network resource from a ``NetworkState``.
|
||||
///
|
||||
/// Used when translating from the internal plugin-protocol type to the
|
||||
/// public API surface type.
|
||||
public init(_ networkState: NetworkState) {
|
||||
switch networkState {
|
||||
case .created(let config):
|
||||
self.init(config: config)
|
||||
case .running(let config, let status):
|
||||
self.init(config: config, networkStatus: status)
|
||||
}
|
||||
/// - configuration: The network's intrinsic configuration.
|
||||
/// - status: The runtime status reported by the network plugin.
|
||||
public init(configuration: NetworkConfiguration, status: NetworkStatus) {
|
||||
self.configuration = configuration
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,48 +77,20 @@ extension NetworkResource {
|
||||
extension NetworkResource {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case state
|
||||
case configuration
|
||||
case status
|
||||
}
|
||||
|
||||
private enum StatusCodingKeys: String, CodingKey {
|
||||
case ipv4Subnet
|
||||
case ipv4Gateway
|
||||
case ipv6Subnet
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(status.phase, forKey: .state)
|
||||
try container.encode(config, forKey: .configuration)
|
||||
if status.phase == "running" {
|
||||
var statusContainer = container.nestedContainer(keyedBy: StatusCodingKeys.self, forKey: .status)
|
||||
try statusContainer.encodeIfPresent(status.ipv4Subnet, forKey: .ipv4Subnet)
|
||||
try statusContainer.encodeIfPresent(status.ipv4Gateway, forKey: .ipv4Gateway)
|
||||
try statusContainer.encodeIfPresent(status.ipv6Subnet, forKey: .ipv6Subnet)
|
||||
} else {
|
||||
try container.encodeNil(forKey: .status)
|
||||
}
|
||||
try container.encode(configuration, forKey: .configuration)
|
||||
try container.encode(status, forKey: .status)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let state = try container.decode(String.self, forKey: .state)
|
||||
let config = try container.decode(NetworkConfiguration.self, forKey: .configuration)
|
||||
if try container.decodeNil(forKey: .status) {
|
||||
self.config = config
|
||||
self.status = NetworkStatus(phase: state)
|
||||
} else {
|
||||
let statusContainer = try container.nestedContainer(keyedBy: StatusCodingKeys.self, forKey: .status)
|
||||
self.config = config
|
||||
self.status = NetworkStatus(
|
||||
phase: state,
|
||||
ipv4Subnet: try statusContainer.decodeIfPresent(CIDRv4.self, forKey: .ipv4Subnet),
|
||||
ipv4Gateway: try statusContainer.decodeIfPresent(IPv4Address.self, forKey: .ipv4Gateway),
|
||||
ipv6Subnet: try statusContainer.decodeIfPresent(CIDRv6.self, forKey: .ipv6Subnet)
|
||||
)
|
||||
}
|
||||
configuration = try container.decode(NetworkConfiguration.self, forKey: .configuration)
|
||||
status = try container.decode(NetworkStatus.self, forKey: .status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// 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 ContainerizationExtras
|
||||
import Foundation
|
||||
|
||||
public struct NetworkPluginStatus: Codable, Sendable {
|
||||
/// The address allocated for the network if no subnet was specified at
|
||||
/// creation time; otherwise, the subnet from the configuration.
|
||||
public let ipv4Subnet: CIDRv4
|
||||
|
||||
/// The gateway IPv4 address.
|
||||
public let ipv4Gateway: IPv4Address
|
||||
|
||||
/// The address allocated for the IPv6 network if no subnet was specified at
|
||||
/// creation time; otherwise, the IPv6 subnet from the configuration.
|
||||
/// The value is nil if the IPv6 subnet cannot be determined at creation time.
|
||||
public let ipv6Subnet: CIDRv6?
|
||||
|
||||
public init(
|
||||
ipv4Subnet: CIDRv4,
|
||||
ipv4Gateway: IPv4Address,
|
||||
ipv6Subnet: CIDRv6?,
|
||||
) {
|
||||
self.ipv4Subnet = ipv4Subnet
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.ipv6Subnet = ipv6Subnet
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case ipv4Subnet
|
||||
case ipv4Gateway
|
||||
case ipv6Subnet
|
||||
// TODO: retain for deserialization compatibility for now, remove later
|
||||
case address
|
||||
case gateway
|
||||
}
|
||||
|
||||
/// Create a configuration from the supplied Decoder, initializing missing
|
||||
/// values where possible to reasonable defaults.
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let address = try? container.decode(CIDRv4.self, forKey: .ipv4Subnet) {
|
||||
ipv4Subnet = address
|
||||
} else {
|
||||
ipv4Subnet = try container.decode(CIDRv4.self, forKey: .address)
|
||||
}
|
||||
if let gateway = try? container.decode(IPv4Address.self, forKey: .ipv4Gateway) {
|
||||
ipv4Gateway = gateway
|
||||
} else {
|
||||
ipv4Gateway = try container.decode(IPv4Address.self, forKey: .gateway)
|
||||
}
|
||||
ipv6Subnet = try container.decodeIfPresent(String.self, forKey: .ipv6Subnet)
|
||||
.map { try CIDRv6($0) }
|
||||
}
|
||||
|
||||
/// Encode the configuration to the supplied Encoder.
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(ipv4Subnet, forKey: .ipv4Subnet)
|
||||
try container.encode(ipv4Gateway, forKey: .ipv4Gateway)
|
||||
try container.encodeIfPresent(ipv6Subnet, forKey: .ipv6Subnet)
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration and runtime attributes for a network.
|
||||
public enum NetworkState: Codable, Sendable {
|
||||
// The network has been configured.
|
||||
case created(NetworkConfiguration)
|
||||
// The network is running.
|
||||
case running(NetworkConfiguration, NetworkPluginStatus)
|
||||
|
||||
public var state: String {
|
||||
switch self {
|
||||
case .created: "created"
|
||||
case .running: "running"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
case .created(let config), .running(let config, _): config.id
|
||||
}
|
||||
}
|
||||
|
||||
public var creationDate: Date {
|
||||
switch self {
|
||||
case .created(let config), .running(let config, _): config.creationDate
|
||||
}
|
||||
}
|
||||
|
||||
public var isBuiltin: Bool {
|
||||
switch self {
|
||||
case .created(let config), .running(let config, _): config.labels.isBuiltin
|
||||
}
|
||||
}
|
||||
|
||||
public var pluginInfo: NetworkPluginInfo? {
|
||||
switch self {
|
||||
case .created(let configuration), .running(let configuration, _): configuration.pluginInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,55 +15,26 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
|
||||
/// The runtime status of a network resource.
|
||||
///
|
||||
/// `phase` names the current lifecycle stage; the address fields are present
|
||||
/// only when `phase` is `"running"` and are `nil` otherwise. Clients should
|
||||
/// treat unrecognised `phase` values as unknown forward-compatible stages rather
|
||||
/// than treating them as errors.
|
||||
/// The runtime status of a network — the addresses assigned once the network
|
||||
/// plugin is active. Only present after the network has started.
|
||||
public struct NetworkStatus: Codable, Sendable {
|
||||
/// The current lifecycle phase of the network.
|
||||
///
|
||||
/// Defined values: `"created"` (configured, plugin not yet active) and
|
||||
/// `"running"` (plugin active, subnet and gateway assigned).
|
||||
public let phase: String
|
||||
/// The IPv4 subnet assigned to the network.
|
||||
public let ipv4Subnet: CIDRv4
|
||||
|
||||
/// The allocated IPv4 subnet. Present only when `phase` is `"running"`.
|
||||
public let ipv4Subnet: CIDRv4?
|
||||
/// The IPv4 gateway address.
|
||||
public let ipv4Gateway: IPv4Address
|
||||
|
||||
/// The IPv4 gateway address. Present only when `phase` is `"running"`.
|
||||
public let ipv4Gateway: IPv4Address?
|
||||
|
||||
/// The allocated IPv6 subnet. Present only when `phase` is `"running"` and
|
||||
/// the network has IPv6 enabled.
|
||||
/// The IPv6 subnet assigned to the network, if IPv6 is enabled.
|
||||
public let ipv6Subnet: CIDRv6?
|
||||
|
||||
public init(
|
||||
phase: String,
|
||||
ipv4Subnet: CIDRv4? = nil,
|
||||
ipv4Gateway: IPv4Address? = nil,
|
||||
ipv6Subnet: CIDRv6? = nil
|
||||
ipv4Subnet: CIDRv4,
|
||||
ipv4Gateway: IPv4Address,
|
||||
ipv6Subnet: CIDRv6?
|
||||
) {
|
||||
self.phase = phase
|
||||
self.ipv4Subnet = ipv4Subnet
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.ipv6Subnet = ipv6Subnet
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworkStatus {
|
||||
/// The status value for a network that is configured but not yet running.
|
||||
public static let created = NetworkStatus(phase: "created")
|
||||
|
||||
/// Creates a running-phase status from a ``NetworkPluginStatus``.
|
||||
init(running networkStatus: NetworkPluginStatus) {
|
||||
self.init(
|
||||
phase: "running",
|
||||
ipv4Subnet: networkStatus.ipv4Subnet,
|
||||
ipv4Gateway: networkStatus.ipv4Gateway,
|
||||
ipv6Subnet: networkStatus.ipv6Subnet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,17 +82,14 @@ extension NetworkVmnetHelper {
|
||||
log.info("configuring XPC server")
|
||||
let ipv4Subnet = try self.ipv4Subnet.map { try CIDRv4($0) }
|
||||
let ipv6Subnet = try self.ipv6Subnet.map { try CIDRv6($0) }
|
||||
let pluginInfo = NetworkPluginInfo(
|
||||
plugin: NetworkVmnetHelper._commandName,
|
||||
variant: self.variant.rawValue
|
||||
)
|
||||
|
||||
let configuration = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: mode,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
ipv6Subnet: ipv6Subnet,
|
||||
pluginInfo: pluginInfo
|
||||
plugin: NetworkVmnetHelper._commandName,
|
||||
options: ["variant": self.variant.rawValue]
|
||||
)
|
||||
let network = try Self.createNetwork(
|
||||
configuration: configuration,
|
||||
@@ -105,7 +102,7 @@ extension NetworkVmnetHelper {
|
||||
let xpc = XPCServer(
|
||||
identifier: serviceIdentifier,
|
||||
routes: [
|
||||
NetworkRoutes.state.rawValue: XPCServer.route(harness.state),
|
||||
NetworkRoutes.status.rawValue: XPCServer.route(harness.status),
|
||||
NetworkRoutes.allocate.rawValue: harness.allocate,
|
||||
NetworkRoutes.lookup.rawValue: XPCServer.route(harness.lookup),
|
||||
],
|
||||
|
||||
@@ -64,11 +64,11 @@ extension RuntimeLinuxHelper {
|
||||
signal(SIGPIPE, SIG_IGN)
|
||||
|
||||
// FIXME: The network plugins that the runtime supports should be configurable elsewhere
|
||||
var interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy] = [
|
||||
NetworkPluginInfo(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy()
|
||||
var interfaceStrategies: [NetworkInterfaceKey: InterfaceStrategy] = [
|
||||
NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy()
|
||||
]
|
||||
if #available(macOS 26, *) {
|
||||
interfaceStrategies[NetworkPluginInfo(plugin: "container-network-vmnet", variant: "reserved")] = NonisolatedInterfaceStrategy(log: log)
|
||||
interfaceStrategies[NetworkInterfaceKey(plugin: "container-network-vmnet", variant: "reserved")] = NonisolatedInterfaceStrategy(log: log)
|
||||
}
|
||||
|
||||
log.info("configuring XPC server")
|
||||
|
||||
@@ -82,18 +82,10 @@ public struct NetworkClient: Sendable {
|
||||
|
||||
let response = try await xpcSend(message: request)
|
||||
|
||||
// Prefer current encoding (≥ 0.12.0 server).
|
||||
if let resourceData = response.dataNoCopy(key: .networkResource) {
|
||||
return try JSONDecoder().decode(NetworkResource.self, from: resourceData)
|
||||
guard let resourceData = response.dataNoCopy(key: .networkResource) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "network configuration not received")
|
||||
}
|
||||
|
||||
// Fall back to pre-0.12.0 server: decode NetworkState and convert.
|
||||
if let stateData = response.dataNoCopy(key: .networkState) {
|
||||
let state = try JSONDecoder().decode(NetworkState.self, from: stateData)
|
||||
return NetworkResource(state)
|
||||
}
|
||||
|
||||
throw ContainerizationError(.invalidArgument, message: "network configuration not received")
|
||||
return try JSONDecoder().decode(NetworkResource.self, from: resourceData)
|
||||
}
|
||||
|
||||
/// Returns the current state of all networks known to the API server.
|
||||
@@ -106,17 +98,10 @@ public struct NetworkClient: Sendable {
|
||||
|
||||
let response = try await xpcSend(message: request, timeout: .seconds(1))
|
||||
|
||||
// Prefer current encoding (≥ 0.12.0 server).
|
||||
if let resourceData = response.dataNoCopy(key: .networkResources) {
|
||||
return try JSONDecoder().decode([NetworkResource].self, from: resourceData)
|
||||
guard let resourceData = response.dataNoCopy(key: .networkResources) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Fall back to pre-0.12.0 server: decode NetworkState and convert.
|
||||
if let stateData = response.dataNoCopy(key: .networkStates) {
|
||||
return try JSONDecoder().decode([NetworkState].self, from: stateData).map(NetworkResource.init)
|
||||
}
|
||||
|
||||
return []
|
||||
return try JSONDecoder().decode([NetworkResource].self, from: resourceData)
|
||||
}
|
||||
|
||||
/// Returns the network with the given identifier.
|
||||
|
||||
@@ -216,10 +216,7 @@ public struct Utility {
|
||||
dnsDomain: containerSystemConfig.dns.domain,
|
||||
)
|
||||
for attachmentConfiguration in config.networks {
|
||||
let network = try await networkClient.get(id: attachmentConfiguration.network)
|
||||
guard network.status.phase == "running" else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(attachmentConfiguration.network) is not running")
|
||||
}
|
||||
_ = try await networkClient.get(id: attachmentConfiguration.network)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,11 +103,6 @@ public enum XPCKeys: String {
|
||||
/// Network
|
||||
case networkId
|
||||
case networkConfig
|
||||
case networkState
|
||||
case networkStates
|
||||
// Added in 0.12.0: NetworkResource encoding (status.phase shape).
|
||||
// DEPRECATED 0.12.0: networkState/networkStates retained for down-revision
|
||||
// client compatibility; remove at next major version boundary.
|
||||
case networkResource
|
||||
case networkResources
|
||||
|
||||
|
||||
@@ -435,10 +435,10 @@ public actor ContainersService {
|
||||
|
||||
var networkBootstrapInfos = [NetworkBootstrapInfo]()
|
||||
for n in config.networks {
|
||||
guard let pluginInfo = try await self.networksService?.pluginInfo(id: n.network) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to get plugin info for network \(n.network)")
|
||||
guard let (plugin, options) = try await self.networksService?.pluginConfiguration(id: n.network) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to get plugin configuration for network \(n.network)")
|
||||
}
|
||||
networkBootstrapInfos.append(NetworkBootstrapInfo(pluginInfo: pluginInfo))
|
||||
networkBootstrapInfos.append(NetworkBootstrapInfo(plugin: plugin, options: options))
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
@@ -32,18 +32,11 @@ public struct NetworksHarness: Sendable {
|
||||
|
||||
@Sendable
|
||||
public func list(_ message: XPCMessage) async throws -> XPCMessage {
|
||||
let states = try await service.list()
|
||||
let resources = try await service.list()
|
||||
|
||||
let reply = message.reply()
|
||||
|
||||
// Current encoding: NetworkResource with status.phase shape (≥ 0.12.0).
|
||||
let resources = states.map(NetworkResource.init)
|
||||
reply.set(key: .networkResources, value: try JSONEncoder().encode(resources))
|
||||
|
||||
// DEPRECATED 0.12.0 — retained for down-revision client compatibility.
|
||||
// Remove at next major version boundary.
|
||||
reply.set(key: .networkStates, value: try JSONEncoder().encode(states))
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
@@ -55,16 +48,10 @@ public struct NetworksHarness: Sendable {
|
||||
}
|
||||
|
||||
let config = try JSONDecoder().decode(NetworkConfiguration.self, from: data)
|
||||
let networkState = try await service.create(configuration: config)
|
||||
let resource = try await service.create(configuration: config)
|
||||
|
||||
let reply = message.reply()
|
||||
|
||||
// Current encoding: NetworkResource with status.phase shape (≥ 0.12.0).
|
||||
reply.set(key: .networkResource, value: try JSONEncoder().encode(NetworkResource(networkState)))
|
||||
|
||||
// DEPRECATED 0.12.0 — retained for down-revision client compatibility.
|
||||
// Remove at next major version boundary.
|
||||
reply.set(key: .networkState, value: try JSONEncoder().encode(networkState))
|
||||
reply.set(key: .networkResource, value: try JSONEncoder().encode(resource))
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ import Logging
|
||||
import SystemPackage
|
||||
|
||||
public actor NetworksService {
|
||||
struct NetworkServiceState {
|
||||
var networkState: NetworkState
|
||||
struct NetworkEntry {
|
||||
var configuration: NetworkConfiguration
|
||||
var status: NetworkStatus
|
||||
var client: ContainerNetworkClient.NetworkClient
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ public actor NetworksService {
|
||||
private var busyNetworks = Set<String>()
|
||||
|
||||
private let stateLock = AsyncLock()
|
||||
private var serviceStates = [String: NetworkServiceState]()
|
||||
private var serviceStates = [String: NetworkEntry]()
|
||||
|
||||
public init(
|
||||
pluginLoader: PluginLoader,
|
||||
@@ -88,26 +89,45 @@ public actor NetworksService {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the network always has plugin information.
|
||||
// Before this field was added, the code always assumed we were using the
|
||||
// container-network-vmnet network plugin, so it should be safe to fallback to that
|
||||
// if no info was found in an on disk configuration.
|
||||
if updatedLabels != nil || configuration.pluginInfo == nil {
|
||||
if let updatedLabels {
|
||||
let updatedConfiguration = try NetworkConfiguration(
|
||||
id: configuration.id,
|
||||
mode: configuration.mode,
|
||||
ipv4Subnet: configuration.ipv4Subnet,
|
||||
ipv6Subnet: configuration.ipv6Subnet,
|
||||
labels: updatedLabels.map { try .init($0) } ?? configuration.labels,
|
||||
pluginInfo: configuration.pluginInfo ?? NetworkPluginInfo(plugin: "container-network-vmnet")
|
||||
labels: try .init(updatedLabels),
|
||||
plugin: configuration.plugin,
|
||||
options: configuration.options
|
||||
)
|
||||
try await store.update(updatedConfiguration)
|
||||
|
||||
}
|
||||
|
||||
// Start up the network.
|
||||
// This call will normally take ~20-100ms to complete after service
|
||||
// registration, but on a fresh system (e.g. CI runner), it may take
|
||||
// 5 seconds or considerably more from the registration of this first
|
||||
// network service to its execution.
|
||||
do {
|
||||
try await registerService(configuration: configuration)
|
||||
let client = try Self.getClient(configuration: configuration)
|
||||
let networkStatus = try await client.status()
|
||||
let finalConfiguration =
|
||||
updatedLabels.flatMap { labels in
|
||||
try? NetworkConfiguration(
|
||||
id: configuration.id,
|
||||
mode: configuration.mode,
|
||||
ipv4Subnet: configuration.ipv4Subnet,
|
||||
ipv6Subnet: configuration.ipv6Subnet,
|
||||
labels: (try? ResourceLabels(labels)) ?? configuration.labels,
|
||||
plugin: configuration.plugin,
|
||||
options: configuration.options
|
||||
)
|
||||
} ?? configuration
|
||||
serviceStates[finalConfiguration.id] = NetworkEntry(
|
||||
configuration: finalConfiguration,
|
||||
status: networkStatus,
|
||||
client: client
|
||||
)
|
||||
} catch {
|
||||
log.error(
|
||||
"failed to start network",
|
||||
@@ -116,74 +136,21 @@ public actor NetworksService {
|
||||
"error": "\(error)",
|
||||
])
|
||||
}
|
||||
|
||||
// This call will normally take ~20-100ms to complete after service
|
||||
// registration, but on a fresh system (e.g. CI runner), it may take
|
||||
// 5 seconds or considerably more from the registration of this first
|
||||
// network service to its execution.
|
||||
let client = try Self.getClient(configuration: configuration)
|
||||
var networkState = try await client.state()
|
||||
|
||||
// FIXME: Temporary workaround for persisted configuration being overwritten
|
||||
// by what comes back from the network helper, which messes up creationDate.
|
||||
// FIXME: Temporarily need to override the plugin information with the info from
|
||||
// the helper, so we can ensure that older networks get a variant value.
|
||||
let finalConfiguration: NetworkConfiguration
|
||||
switch networkState {
|
||||
case .created(let helperConfig):
|
||||
finalConfiguration = try NetworkConfiguration(
|
||||
id: configuration.id,
|
||||
mode: configuration.mode,
|
||||
ipv4Subnet: configuration.ipv4Subnet,
|
||||
ipv6Subnet: configuration.ipv6Subnet,
|
||||
labels: updatedLabels.map { try .init($0) } ?? configuration.labels,
|
||||
pluginInfo: helperConfig.pluginInfo
|
||||
)
|
||||
networkState = NetworkState.created(finalConfiguration)
|
||||
case .running(let helperConfig, let status):
|
||||
finalConfiguration = try NetworkConfiguration(
|
||||
id: configuration.id,
|
||||
mode: configuration.mode,
|
||||
ipv4Subnet: configuration.ipv4Subnet,
|
||||
ipv6Subnet: configuration.ipv6Subnet,
|
||||
labels: updatedLabels.map { try .init($0) } ?? configuration.labels,
|
||||
pluginInfo: helperConfig.pluginInfo
|
||||
)
|
||||
networkState = NetworkState.running(finalConfiguration, status)
|
||||
}
|
||||
|
||||
let state = NetworkServiceState(
|
||||
networkState: networkState,
|
||||
client: client
|
||||
)
|
||||
|
||||
serviceStates[finalConfiguration.id] = state
|
||||
|
||||
guard case .running = networkState else {
|
||||
log.error(
|
||||
"network failed to start",
|
||||
metadata: [
|
||||
"id": "\(finalConfiguration.id)",
|
||||
"state": "\(networkState.state)",
|
||||
])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all networks registered with the service.
|
||||
public func list() async throws -> [NetworkState] {
|
||||
public func list() async throws -> [NetworkResource] {
|
||||
log.debug("NetworksService: enter", metadata: ["func": "\(#function)"])
|
||||
defer { log.debug("NetworksService: exit", metadata: ["func": "\(#function)"]) }
|
||||
|
||||
return serviceStates.reduce(into: [NetworkState]()) {
|
||||
$0.append($1.value.networkState)
|
||||
}
|
||||
.sorted { $0.id < $1.id }
|
||||
return serviceStates.values
|
||||
.map { NetworkResource(configuration: $0.configuration, status: $0.status) }
|
||||
.sorted { $0.id < $1.id }
|
||||
}
|
||||
|
||||
/// Create a new network from the provided configuration.
|
||||
public func create(configuration: NetworkConfiguration) async throws -> NetworkState {
|
||||
public func create(configuration: NetworkConfiguration) async throws -> NetworkResource {
|
||||
log.debug(
|
||||
"NetworksService: enter",
|
||||
metadata: [
|
||||
@@ -224,11 +191,8 @@ public actor NetworksService {
|
||||
try await self.registerService(configuration: configuration)
|
||||
let client = try Self.getClient(configuration: configuration)
|
||||
|
||||
// Ensure the network is running, and set up the persistent network state
|
||||
// using our configuration data
|
||||
guard case .running(let helperConfig, let status) = try await client.state() else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(configuration.id) failed to start")
|
||||
}
|
||||
// Ensure the network is running
|
||||
let networkStatus = try await client.status()
|
||||
|
||||
let finalConfiguration = try NetworkConfiguration(
|
||||
id: configuration.id,
|
||||
@@ -236,17 +200,17 @@ public actor NetworksService {
|
||||
ipv4Subnet: configuration.ipv4Subnet,
|
||||
ipv6Subnet: configuration.ipv6Subnet,
|
||||
labels: configuration.labels,
|
||||
pluginInfo: helperConfig.pluginInfo
|
||||
plugin: configuration.plugin,
|
||||
options: configuration.options
|
||||
)
|
||||
|
||||
let networkState: NetworkState = .running(finalConfiguration, status)
|
||||
let serviceState = NetworkServiceState(networkState: networkState, client: client)
|
||||
await self.setServiceState(key: finalConfiguration.id, value: serviceState)
|
||||
let entry = NetworkEntry(configuration: finalConfiguration, status: networkStatus, client: client)
|
||||
await self.setServiceState(key: finalConfiguration.id, value: entry)
|
||||
|
||||
// Persist the configuration data.
|
||||
do {
|
||||
try await self.store.create(finalConfiguration)
|
||||
return networkState
|
||||
return NetworkResource(configuration: finalConfiguration, status: networkStatus)
|
||||
} catch {
|
||||
await self.removeServiceState(key: finalConfiguration.id)
|
||||
do {
|
||||
@@ -304,12 +268,8 @@ public actor NetworksService {
|
||||
throw ContainerizationError(.notFound, message: "no network for id \(id)")
|
||||
}
|
||||
|
||||
guard case .running(let netConfig, _) = serviceState.networkState else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete network \(id) in state \(serviceState.networkState.state)")
|
||||
}
|
||||
|
||||
// basic sanity checks on network itself
|
||||
if serviceState.networkState.isBuiltin {
|
||||
if serviceState.configuration.labels.isBuiltin {
|
||||
throw ContainerizationError(.invalidArgument, message: "cannot delete builtin network: \(id)")
|
||||
}
|
||||
|
||||
@@ -336,7 +296,7 @@ public actor NetworksService {
|
||||
|
||||
// start network deletion, this is the last place we'll want to throw
|
||||
do {
|
||||
try await self.deregisterService(configuration: netConfig)
|
||||
try await self.deregisterService(configuration: serviceState.configuration)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to deregister network service",
|
||||
@@ -379,21 +339,23 @@ public actor NetworksService {
|
||||
}
|
||||
}
|
||||
|
||||
public func pluginInfo(id: String) throws -> NetworkPluginInfo {
|
||||
public func pluginConfiguration(id: String) throws -> (plugin: String, options: [String: String]) {
|
||||
guard let serviceState = serviceStates[id] else {
|
||||
throw ContainerizationError(.notFound, message: "no network for id \(id)")
|
||||
}
|
||||
guard let pluginInfo = serviceState.networkState.pluginInfo else {
|
||||
throw ContainerizationError(.internalError, message: "network \(id) missing plugin information")
|
||||
var options = serviceState.configuration.options
|
||||
if options["variant"] == nil {
|
||||
if #available(macOS 26, *) {
|
||||
options["variant"] = "reserved"
|
||||
} else {
|
||||
options["variant"] = "allocationOnly"
|
||||
}
|
||||
}
|
||||
return pluginInfo
|
||||
return (plugin: serviceState.configuration.plugin, options: options)
|
||||
}
|
||||
|
||||
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkClient.NetworkClient {
|
||||
guard let pluginInfo = configuration.pluginInfo else {
|
||||
throw ContainerizationError(.internalError, message: "network \(configuration.id) missing plugin information")
|
||||
}
|
||||
return NetworkClient(id: configuration.id, plugin: pluginInfo.plugin)
|
||||
NetworkClient(id: configuration.id, plugin: configuration.plugin)
|
||||
}
|
||||
|
||||
private func registerService(configuration: NetworkConfiguration) async throws {
|
||||
@@ -401,14 +363,10 @@ public actor NetworksService {
|
||||
throw ContainerizationError(.invalidArgument, message: "unsupported network mode \(configuration.mode.rawValue)")
|
||||
}
|
||||
|
||||
guard let pluginInfo = configuration.pluginInfo else {
|
||||
throw ContainerizationError(.internalError, message: "network \(configuration.id) missing plugin information")
|
||||
}
|
||||
|
||||
guard let networkPlugin = self.networkPlugins.first(where: { $0.name == pluginInfo.plugin }) else {
|
||||
guard let networkPlugin = self.networkPlugins.first(where: { $0.name == configuration.plugin }) else {
|
||||
throw ContainerizationError(
|
||||
.notFound,
|
||||
message: "unable to locate network plugin \(pluginInfo.plugin)"
|
||||
message: "unable to locate network plugin \(configuration.plugin)"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -431,9 +389,7 @@ public actor NetworksService {
|
||||
if let ipv4Subnet = configuration.ipv4Subnet {
|
||||
var existingCidrs: [CIDRv4] = []
|
||||
for serviceState in serviceStates.values {
|
||||
if case .running(_, let status) = serviceState.networkState {
|
||||
existingCidrs.append(status.ipv4Subnet)
|
||||
}
|
||||
existingCidrs.append(serviceState.status.ipv4Subnet)
|
||||
}
|
||||
let overlap = existingCidrs.first {
|
||||
$0.contains(ipv4Subnet.lower)
|
||||
@@ -451,7 +407,7 @@ public actor NetworksService {
|
||||
if let ipv6Subnet = configuration.ipv6Subnet {
|
||||
var existingCidrs: [CIDRv6] = []
|
||||
for serviceState in serviceStates.values {
|
||||
if case .running(_, let status) = serviceState.networkState, let otherIPv6Subnet = status.ipv6Subnet {
|
||||
if let otherIPv6Subnet = serviceState.status.ipv6Subnet {
|
||||
existingCidrs.append(otherIPv6Subnet)
|
||||
}
|
||||
}
|
||||
@@ -468,7 +424,7 @@ public actor NetworksService {
|
||||
args += ["--subnet-v6", ipv6Subnet.description]
|
||||
}
|
||||
|
||||
if let variant = configuration.pluginInfo?.variant {
|
||||
if let variant = configuration.options["variant"] {
|
||||
args += ["--variant", variant]
|
||||
}
|
||||
|
||||
@@ -482,13 +438,10 @@ public actor NetworksService {
|
||||
}
|
||||
|
||||
private func deregisterService(configuration: NetworkConfiguration) async throws {
|
||||
guard let pluginInfo = configuration.pluginInfo else {
|
||||
throw ContainerizationError(.internalError, message: "network \(configuration.id) missing plugin information")
|
||||
}
|
||||
guard let networkPlugin = self.networkPlugins.first(where: { $0.name == pluginInfo.plugin }) else {
|
||||
guard let networkPlugin = self.networkPlugins.first(where: { $0.name == configuration.plugin }) else {
|
||||
throw ContainerizationError(
|
||||
.notFound,
|
||||
message: "unable to locate network plugin \(pluginInfo.plugin)"
|
||||
message: "unable to locate network plugin \(configuration.plugin)"
|
||||
)
|
||||
}
|
||||
try self.pluginLoader.deregisterWithLaunchd(plugin: networkPlugin, instanceId: configuration.id)
|
||||
@@ -500,7 +453,7 @@ extension NetworksService {
|
||||
self.serviceStates.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
private func setServiceState(key: String, value: NetworkServiceState) {
|
||||
private func setServiceState(key: String, value: NetworkEntry) {
|
||||
self.serviceStates[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,33 +44,6 @@ public struct NetworkClient: Sendable {
|
||||
|
||||
// Runtime Methods
|
||||
extension NetworkClient {
|
||||
public func state() async throws -> NetworkState {
|
||||
let request = XPCMessage(route: NetworkRoutes.state.rawValue)
|
||||
let client = createClient()
|
||||
|
||||
let response = try await client.send(request)
|
||||
let state = try response.state()
|
||||
return state
|
||||
}
|
||||
|
||||
public func allocate(
|
||||
hostname: String,
|
||||
macAddress: MACAddress? = nil
|
||||
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
|
||||
let request = XPCMessage(route: NetworkRoutes.allocate.rawValue)
|
||||
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
|
||||
if let macAddress = macAddress {
|
||||
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description)
|
||||
}
|
||||
|
||||
let client = createClient()
|
||||
|
||||
let response = try await client.send(request)
|
||||
let attachment = try response.attachment()
|
||||
let additionalData = response.additionalData()
|
||||
return (attachment, additionalData)
|
||||
}
|
||||
|
||||
/// Open a persistent connection to the network helper.
|
||||
///
|
||||
/// The returned session should be reused for `allocate(on:)` calls. The
|
||||
@@ -80,6 +53,15 @@ extension NetworkClient {
|
||||
createClient().openSession()
|
||||
}
|
||||
|
||||
public func status() async throws -> NetworkStatus {
|
||||
let request = XPCMessage(route: NetworkRoutes.status.rawValue)
|
||||
let client = createClient()
|
||||
|
||||
let response = try await client.send(request)
|
||||
let status = try response.status()
|
||||
return status
|
||||
}
|
||||
|
||||
/// Allocate a network attachment over an existing session.
|
||||
///
|
||||
/// Use `connect()` to obtain a session, then pass it here. The session
|
||||
@@ -142,11 +124,11 @@ extension XPCMessage {
|
||||
return hostname
|
||||
}
|
||||
|
||||
public func state() throws -> NetworkState {
|
||||
let data = self.dataNoCopy(key: NetworkKeys.state.rawValue)
|
||||
public func status() throws -> NetworkStatus {
|
||||
let data = self.dataNoCopy(key: NetworkKeys.status.rawValue)
|
||||
guard let data else {
|
||||
throw ContainerizationError(.invalidArgument, message: "no network snapshot data in message")
|
||||
}
|
||||
return try JSONDecoder().decode(NetworkState.self, from: data)
|
||||
return try JSONDecoder().decode(NetworkStatus.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ public enum NetworkKeys: String {
|
||||
case hostname
|
||||
case macAddress
|
||||
case network
|
||||
case state
|
||||
case status
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public enum NetworkRoutes: String {
|
||||
/// Return the current state of the network.
|
||||
case state = "com.apple.container.network/state"
|
||||
/// Return the current status of the network.
|
||||
case status = "com.apple.container.network/status"
|
||||
/// Allocates parameters for attaching a sandbox to the network.
|
||||
case allocate = "com.apple.container.network/allocate"
|
||||
/// Retrieves the allocation for a hostname.
|
||||
|
||||
@@ -32,13 +32,11 @@ public actor DefaultNetworkService: NetworkService {
|
||||
network: any Network,
|
||||
log: Logger
|
||||
) async throws {
|
||||
let state = await network.state
|
||||
guard case .running(_, let status) = state else {
|
||||
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
|
||||
guard let status = await network.status else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(network.id) must be running")
|
||||
}
|
||||
|
||||
let subnet = status.ipv4Subnet
|
||||
|
||||
let size = Int(subnet.upper.value - subnet.lower.value - 3)
|
||||
self.network = network
|
||||
self.log = log
|
||||
@@ -48,8 +46,11 @@ public actor DefaultNetworkService: NetworkService {
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public func state() async throws -> NetworkState {
|
||||
await network.state
|
||||
public func status() async throws -> NetworkStatus {
|
||||
guard let status = await network.status else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(network.id) is not running")
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
@Sendable
|
||||
@@ -61,9 +62,8 @@ public actor DefaultNetworkService: NetworkService {
|
||||
log.debug("enter", metadata: ["func": "\(#function)"])
|
||||
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
|
||||
|
||||
let state = await network.state
|
||||
guard case .running(_, let status) = state else {
|
||||
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
|
||||
guard let status = await network.status else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(network.id) must be running")
|
||||
}
|
||||
|
||||
let macAddress = macAddress ?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000)
|
||||
@@ -72,7 +72,7 @@ public actor DefaultNetworkService: NetworkService {
|
||||
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
|
||||
let ip = IPv4Address(index)
|
||||
let attachment = Attachment(
|
||||
network: state.id,
|
||||
network: network.id,
|
||||
hostname: hostname,
|
||||
ipv4Address: try CIDRv4(ip, prefix: status.ipv4Subnet.prefix),
|
||||
ipv4Gateway: status.ipv4Gateway,
|
||||
@@ -122,9 +122,8 @@ public actor DefaultNetworkService: NetworkService {
|
||||
log.debug("enter", metadata: ["func": "\(#function)"])
|
||||
defer { log.debug("exit", metadata: ["func": "\(#function)"]) }
|
||||
|
||||
let state = await network.state
|
||||
guard case .running(_, let status) = state else {
|
||||
throw ContainerizationError(.invalidState, message: "invalid network state - network \(state.id) must be running")
|
||||
guard let status = await network.status else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(network.id) must be running")
|
||||
}
|
||||
|
||||
// Invariant: hostname -> index if and only if index -> MAC address
|
||||
@@ -136,14 +135,13 @@ public actor DefaultNetworkService: NetworkService {
|
||||
return nil
|
||||
}
|
||||
|
||||
// populate attachment
|
||||
let address = IPv4Address(index)
|
||||
let subnet = status.ipv4Subnet
|
||||
let ipv4Address = try CIDRv4(address, prefix: subnet.prefix)
|
||||
let ipv6Address = try status.ipv6Subnet
|
||||
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
|
||||
let attachment = Attachment(
|
||||
network: state.id,
|
||||
network: network.id,
|
||||
hostname: hostname,
|
||||
ipv4Address: ipv4Address,
|
||||
ipv4Gateway: status.ipv4Gateway,
|
||||
|
||||
@@ -19,12 +19,15 @@ import ContainerXPC
|
||||
|
||||
/// Defines common characteristics and operations for a network.
|
||||
public protocol Network: Sendable {
|
||||
// Contains network attributes while the network is running
|
||||
var state: NetworkState { get async }
|
||||
/// The network's identifier.
|
||||
var id: String { get }
|
||||
|
||||
// Use implementation-dependent network attributes
|
||||
/// The network's runtime status. `nil` before ``start()`` completes.
|
||||
var status: NetworkStatus? { get async }
|
||||
|
||||
/// Use implementation-dependent network attributes.
|
||||
nonisolated func withAdditionalData(_ handler: (XPCMessage?) throws -> Void) throws
|
||||
|
||||
// Start the network
|
||||
/// Start the network.
|
||||
func start() async throws
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ public actor NetworkHarness: Sendable {
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public func state(_ message: XPCMessage) async throws -> XPCMessage {
|
||||
public func status(_ message: XPCMessage) async throws -> XPCMessage {
|
||||
let reply = message.reply()
|
||||
let state = try await service.state()
|
||||
try reply.setState(state)
|
||||
let status = try await service.status()
|
||||
try reply.setStatus(status)
|
||||
return reply
|
||||
}
|
||||
|
||||
@@ -80,8 +80,9 @@ extension XPCMessage {
|
||||
self.set(key: NetworkKeys.attachment.rawValue, value: data)
|
||||
}
|
||||
|
||||
fileprivate func setState(_ state: NetworkState) throws {
|
||||
let data = try JSONEncoder().encode(state)
|
||||
self.set(key: NetworkKeys.state.rawValue, value: data)
|
||||
fileprivate func setStatus(_ status: NetworkStatus) throws {
|
||||
let data = try JSONEncoder().encode(status)
|
||||
self.set(key: NetworkKeys.status.rawValue, value: data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import ContainerizationExtras
|
||||
/// A network service
|
||||
public protocol NetworkService: Sendable {
|
||||
/// Gets the properties of the realized network.
|
||||
func state() async throws -> NetworkState
|
||||
func status() async throws -> NetworkStatus
|
||||
|
||||
/// Register a hostname and allocate associated addresses.
|
||||
func allocate(
|
||||
|
||||
@@ -25,8 +25,9 @@ public actor AllocationOnlyVmnetNetwork: Network {
|
||||
// The IPv4 subnet to be used if none explicitly passed in the `NetworkConfiguration`
|
||||
private static let defaultIPv4Subnet = try! CIDRv4("192.168.64.1/24")
|
||||
|
||||
private let configuration: NetworkConfiguration
|
||||
private let log: Logger
|
||||
private var _state: NetworkState
|
||||
private var _status: NetworkStatus?
|
||||
|
||||
/// Configure a bridge network that allows external system access using
|
||||
/// network address translation.
|
||||
@@ -42,21 +43,22 @@ public actor AllocationOnlyVmnetNetwork: Network {
|
||||
throw ContainerizationError(.unsupported, message: "IPv6 subnet assignment is not yet implemented")
|
||||
}
|
||||
|
||||
self.configuration = configuration
|
||||
self.log = log
|
||||
self._state = .created(configuration)
|
||||
self._status = nil
|
||||
}
|
||||
|
||||
public var state: NetworkState {
|
||||
self._state
|
||||
}
|
||||
public nonisolated var id: String { configuration.id }
|
||||
|
||||
public var status: NetworkStatus? { _status }
|
||||
|
||||
public nonisolated func withAdditionalData(_ handler: (XPCMessage?) throws -> Void) throws {
|
||||
try handler(nil)
|
||||
}
|
||||
|
||||
public func start() async throws {
|
||||
guard case .created(let configuration) = _state else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot start network \(_state.id) in \(_state.state) state")
|
||||
guard _status == nil else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot start network \(configuration.id): already started")
|
||||
}
|
||||
|
||||
log.info(
|
||||
@@ -68,14 +70,12 @@ public actor AllocationOnlyVmnetNetwork: Network {
|
||||
)
|
||||
|
||||
let ipv4Subnet = configuration.ipv4Subnet ?? Self.defaultIPv4Subnet
|
||||
|
||||
let gateway = IPv4Address(ipv4Subnet.lower.value + 1)
|
||||
let status = NetworkPluginStatus(
|
||||
self._status = NetworkStatus(
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
ipv4Gateway: gateway,
|
||||
ipv6Subnet: nil,
|
||||
ipv6Subnet: nil
|
||||
)
|
||||
self._state = .running(configuration, status)
|
||||
log.info(
|
||||
"started allocation-only network",
|
||||
metadata: [
|
||||
|
||||
@@ -29,7 +29,7 @@ import vmnet
|
||||
@available(macOS 26, *)
|
||||
public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
|
||||
private struct State {
|
||||
var networkState: NetworkState
|
||||
var status: NetworkStatus?
|
||||
var network: vmnet_network_ref?
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
|
||||
let ipv6Subnet: CIDRv6
|
||||
}
|
||||
|
||||
private let configuration: NetworkConfiguration
|
||||
private let stateMutex: Mutex<State>
|
||||
private let log: Logger
|
||||
|
||||
@@ -54,14 +55,16 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
|
||||
}
|
||||
|
||||
log.info("creating vmnet network")
|
||||
self.configuration = configuration
|
||||
self.log = log
|
||||
let initialState = State(networkState: .created(configuration))
|
||||
stateMutex = Mutex(initialState)
|
||||
stateMutex = Mutex(State())
|
||||
log.info("created vmnet network")
|
||||
}
|
||||
|
||||
public var state: NetworkState {
|
||||
stateMutex.withLock { $0.networkState }
|
||||
public nonisolated var id: String { configuration.id }
|
||||
|
||||
public var status: NetworkStatus? {
|
||||
stateMutex.withLock { $0.status }
|
||||
}
|
||||
|
||||
public nonisolated func withAdditionalData(_ handler: (XPCMessage?) throws -> Void) throws {
|
||||
@@ -72,18 +75,17 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
|
||||
|
||||
public func start() async throws {
|
||||
try stateMutex.withLock { state in
|
||||
guard case .created(let configuration) = state.networkState else {
|
||||
throw ContainerizationError(.invalidArgument, message: "cannot start network that is in \(state.networkState.state) state")
|
||||
guard state.status == nil else {
|
||||
throw ContainerizationError(.invalidArgument, message: "cannot start network \(configuration.id): already started")
|
||||
}
|
||||
|
||||
let networkInfo = try startNetwork(configuration: configuration, log: log)
|
||||
|
||||
let networkStatus = NetworkPluginStatus(
|
||||
state.status = NetworkStatus(
|
||||
ipv4Subnet: networkInfo.ipv4Subnet,
|
||||
ipv4Gateway: networkInfo.ipv4Gateway,
|
||||
ipv6Subnet: networkInfo.ipv6Subnet,
|
||||
ipv6Subnet: networkInfo.ipv6Subnet
|
||||
)
|
||||
state.networkState = NetworkState.running(configuration, networkStatus)
|
||||
state.network = networkInfo.network
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,17 @@ import ContainerResource
|
||||
import ContainerXPC
|
||||
import Containerization
|
||||
|
||||
/// Key identifying which interface strategy to use for a network attachment.
|
||||
public struct NetworkInterfaceKey: Hashable, Sendable {
|
||||
public let plugin: String
|
||||
public let variant: String?
|
||||
|
||||
public init(plugin: String, variant: String?) {
|
||||
self.plugin = plugin
|
||||
self.variant = variant
|
||||
}
|
||||
}
|
||||
|
||||
/// A strategy for mapping network attachment information to a network interface.
|
||||
public protocol InterfaceStrategy: Sendable {
|
||||
/// Map a client network attachment request to a network interface specification.
|
||||
|
||||
@@ -19,11 +19,14 @@ import ContainerResource
|
||||
/// Plugin info passed from the API server in the sandbox bootstrap message so the
|
||||
/// runtime can connect to the correct network helper and configure the interface.
|
||||
public struct NetworkBootstrapInfo: Codable, Sendable {
|
||||
/// Plugin info identifying which network helper to contact and which interface
|
||||
/// strategy the runtime should use.
|
||||
public let pluginInfo: NetworkPluginInfo
|
||||
/// The network plugin name identifying which network helper to contact.
|
||||
public let plugin: String
|
||||
|
||||
public init(pluginInfo: NetworkPluginInfo) {
|
||||
self.pluginInfo = pluginInfo
|
||||
/// Plugin-specific options, including `variant` which selects the interface strategy.
|
||||
public let options: [String: String]
|
||||
|
||||
public init(plugin: String, options: [String: String] = [:]) {
|
||||
self.plugin = plugin
|
||||
self.options = options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import struct ContainerizationOCI.Process
|
||||
public actor RuntimeService {
|
||||
private let connection: xpc_connection_t
|
||||
private let root: URL
|
||||
private let interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy]
|
||||
private let interfaceStrategies: [NetworkInterfaceKey: InterfaceStrategy]
|
||||
private var container: ContainerInfo?
|
||||
private let monitor: ExitMonitor
|
||||
private let eventLoopGroup: any EventLoopGroup
|
||||
@@ -96,7 +96,7 @@ public actor RuntimeService {
|
||||
|
||||
public init(
|
||||
root: URL,
|
||||
interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy],
|
||||
interfaceStrategies: [NetworkInterfaceKey: InterfaceStrategy],
|
||||
eventLoopGroup: any EventLoopGroup,
|
||||
connection: xpc_connection_t,
|
||||
log: Logger
|
||||
@@ -177,7 +177,7 @@ public actor RuntimeService {
|
||||
do {
|
||||
for (index, info) in networkBootstrapInfos.enumerated() {
|
||||
let attachmentConfig = config.networks[index]
|
||||
let client = ContainerNetworkClient.NetworkClient(id: attachmentConfig.network, plugin: info.pluginInfo.plugin)
|
||||
let client = ContainerNetworkClient.NetworkClient(id: attachmentConfig.network, plugin: info.plugin)
|
||||
let session = client.connect()
|
||||
sessions.append(session)
|
||||
var (attachment, additionalData) = try await client.allocate(
|
||||
@@ -196,9 +196,10 @@ public actor RuntimeService {
|
||||
mtu: mtu
|
||||
)
|
||||
}
|
||||
guard let iStrategy = self.interfaceStrategies[info.pluginInfo] else {
|
||||
guard let iStrategy = self.interfaceStrategies[NetworkInterfaceKey(plugin: info.plugin, variant: info.options["variant"])] else {
|
||||
throw ContainerizationError(
|
||||
.internalError, message: "no available interface strategy for network \(attachment.network), \(info.pluginInfo)")
|
||||
.internalError,
|
||||
message: "no available interface strategy for network \(attachment.network), plugin=\(info.plugin) variant=\(info.options["variant"] ?? "nil")")
|
||||
}
|
||||
let interface = try iStrategy.toInterface(
|
||||
attachment: attachment,
|
||||
|
||||
@@ -290,7 +290,7 @@ class TestCLINetwork: CLITest {
|
||||
let (_, output, error, status) = try run(arguments: ["network", "list"])
|
||||
#expect(status == 0, "network list should succeed, stderr: \(error)")
|
||||
|
||||
let headers = ["NETWORK", "STATE", "SUBNET"]
|
||||
let headers = ["NETWORK", "SUBNET"]
|
||||
#expect(headers.allSatisfy { output.contains($0) }, "table should contain all headers")
|
||||
#expect(output.contains(name), "table should contain the created network")
|
||||
}
|
||||
|
||||
@@ -53,9 +53,8 @@ class CLITest {
|
||||
let ipv6Subnet: String?
|
||||
}
|
||||
let id: String
|
||||
let state: String
|
||||
let configuration: NetworkConfiguration
|
||||
let status: Status?
|
||||
let status: Status
|
||||
}
|
||||
|
||||
let testName: String
|
||||
|
||||
@@ -240,9 +240,9 @@ struct PrintableContainerDisplayTests {
|
||||
|
||||
struct NetworkResourceDisplayTests {
|
||||
@Test
|
||||
func tableHeaderHasThreeColumns() {
|
||||
#expect(NetworkResource.tableHeader.count == 3)
|
||||
#expect(NetworkResource.tableHeader == ["NETWORK", "STATE", "SUBNET"])
|
||||
func tableHeaderHasTwoColumns() {
|
||||
#expect(NetworkResource.tableHeader.count == 2)
|
||||
#expect(NetworkResource.tableHeader == ["NETWORK", "SUBNET"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,12 @@ import Testing
|
||||
@testable import ContainerResource
|
||||
|
||||
struct NetworkConfigurationTest {
|
||||
let defaultNetworkPluginInfo = NetworkPluginInfo(plugin: "container-network-vmnet")
|
||||
|
||||
@Test func testValidationOkDefaults() throws {
|
||||
let id = "foo"
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
plugin: "container-network-vmnet"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +47,7 @@ struct NetworkConfigurationTest {
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
plugin: "container-network-vmnet"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +71,7 @@ struct NetworkConfigurationTest {
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
plugin: "container-network-vmnet"
|
||||
)
|
||||
} throws: { error in
|
||||
guard let err = error as? ContainerizationError else { return false }
|
||||
|
||||
@@ -753,7 +753,7 @@ Creates a new network with the given name.
|
||||
**Usage**
|
||||
|
||||
```bash
|
||||
container network create [--label <label> ...] [--subnet <subnet>] [--subnet-v6 <subnet-v6>] [--debug] <name>
|
||||
container network create [--label <label> ...] [--subnet <subnet>] [--subnet-v6 <subnet-v6>] [--plugin <plugin>] [--option <key=value> ...] [--debug] <name>
|
||||
```
|
||||
|
||||
**Arguments**
|
||||
@@ -765,6 +765,8 @@ container network create [--label <label> ...] [--subnet <subnet>] [--subnet-v6
|
||||
* `--label <label>`: Set metadata for a network
|
||||
* `--subnet <subnet>`: Set the IPv4 subnet for a network (CIDR format, e.g., 192.168.100.0/24)
|
||||
* `--subnet-v6 <subnet-v6>`: Set the IPv6 prefix for a network (CIDR format, e.g., fd00:1234::/64)
|
||||
* `--plugin <plugin>`: Network plugin to use (default: `container-network-vmnet`)
|
||||
* `--option <key=value>`: Set a plugin-specific option; may be repeated
|
||||
|
||||
### `container network delete (rm)`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user