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:
J Logan
2026-05-29 12:33:45 -07:00
committed by GitHub
parent f4f5925c08
commit 37595a734c
33 changed files with 265 additions and 530 deletions
+1 -1
View File
@@ -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")
}
+1 -2
View File
@@ -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 }
+3 -1
View File
@@ -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)`