mirror of
https://github.com/apple/container.git
synced 2026-09-25 09:05:47 +00:00
Support using multiple different network plugins (#1151)
## Type of Change - [x] New feature - [x] Breaking change ## Motivation and Context We want to be able to support using multiple network plugins during `container`'s lifetime. This additionally means needing to pick an interface strategy to interpret a network attachment based on what network plugin was used to create that attachment. This PR will potentially replace https://github.com/apple/container/pull/1081. Followups: - doc updates to include the ability to specify plugin in the network creation cli ## Testing - [x] Tested locally - [x] Added/updated tests
This commit is contained in:
+5
-2
@@ -310,7 +310,6 @@ let package = Package(
|
||||
.product(name: "ContainerizationOS", package: "containerization"),
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
"ContainerAPIClient",
|
||||
"ContainerNetworkServiceClient",
|
||||
"ContainerPersistence",
|
||||
"ContainerResource",
|
||||
"ContainerSandboxServiceClient",
|
||||
@@ -322,6 +321,7 @@ let package = Package(
|
||||
.target(
|
||||
name: "ContainerSandboxServiceClient",
|
||||
dependencies: [
|
||||
"ContainerAPIClient",
|
||||
"ContainerResource",
|
||||
"ContainerXPC",
|
||||
],
|
||||
@@ -330,7 +330,10 @@ let package = Package(
|
||||
.target(
|
||||
name: "ContainerResource",
|
||||
dependencies: [
|
||||
.product(name: "Containerization", package: "containerization")
|
||||
.product(name: "Containerization", package: "containerization"),
|
||||
"ContainerXPC",
|
||||
"CAuditToken",
|
||||
"CVersion",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
||||
@@ -48,6 +48,12 @@ 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,7 +70,8 @@ extension Application {
|
||||
mode: mode,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
ipv6Subnet: ipv6Subnet,
|
||||
labels: parsedLabels
|
||||
labels: parsedLabels,
|
||||
pluginInfo: NetworkPluginInfo(plugin: self.plugin, variant: self.pluginVariant)
|
||||
)
|
||||
let state = try await ClientNetwork.create(configuration: config)
|
||||
print(state.id)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerXPC
|
||||
|
||||
/// AllocatedAttachment represents a network attachment that has been allocated for use
|
||||
/// by a container and any additional relevant data needed for a sandbox to properly
|
||||
/// configure networking on container bootstrap.
|
||||
public struct AllocatedAttachment: Sendable {
|
||||
public let attachment: Attachment
|
||||
public let additionalData: XPCMessage?
|
||||
public let pluginInfo: NetworkPluginInfo
|
||||
|
||||
public init(attachment: Attachment, additionalData: XPCMessage?, pluginInfo: NetworkPluginInfo) {
|
||||
self.attachment = attachment
|
||||
self.additionalData = additionalData
|
||||
self.pluginInfo = pluginInfo
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import ContainerizationExtras
|
||||
|
||||
/// A snapshot of a network interface allocated to a sandbox.
|
||||
/// A snapshot of a network interface for a sandbox.
|
||||
public struct Attachment: Codable, Sendable {
|
||||
/// The network ID associated with the attachment.
|
||||
public let network: String
|
||||
|
||||
@@ -18,6 +18,16 @@ 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
|
||||
@@ -38,13 +48,19 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
/// Key-value labels for the network.
|
||||
public var labels: [String: String] = [:]
|
||||
|
||||
/// 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 var pluginInfo: NetworkPluginInfo?
|
||||
|
||||
/// Creates a network configuration
|
||||
public init(
|
||||
id: String,
|
||||
mode: NetworkMode,
|
||||
ipv4Subnet: CIDRv4? = nil,
|
||||
ipv6Subnet: CIDRv6? = nil,
|
||||
labels: [String: String] = [:]
|
||||
labels: [String: String] = [:],
|
||||
pluginInfo: NetworkPluginInfo,
|
||||
) throws {
|
||||
self.id = id
|
||||
self.creationDate = Date()
|
||||
@@ -52,6 +68,7 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
self.ipv4Subnet = ipv4Subnet
|
||||
self.ipv6Subnet = ipv6Subnet
|
||||
self.labels = labels
|
||||
self.pluginInfo = pluginInfo
|
||||
try validate()
|
||||
}
|
||||
|
||||
@@ -62,6 +79,7 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
case ipv4Subnet
|
||||
case ipv6Subnet
|
||||
case labels
|
||||
case pluginInfo
|
||||
// TODO: retain for deserialization compatability for now, remove later
|
||||
case subnet
|
||||
}
|
||||
@@ -81,6 +99,7 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
|
||||
ipv6Subnet = try container.decodeIfPresent(String.self, forKey: .ipv6Subnet)
|
||||
.map { try CIDRv6($0) }
|
||||
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
|
||||
pluginInfo = try container.decodeIfPresent(NetworkPluginInfo.self, forKey: .pluginInfo)
|
||||
try validate()
|
||||
}
|
||||
|
||||
@@ -94,6 +113,7 @@ 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)
|
||||
}
|
||||
|
||||
private func validate() throws {
|
||||
|
||||
@@ -72,4 +72,10 @@ public enum NetworkState: Codable, Sendable {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,12 @@ extension XPCMessage {
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, xpcDictionary: xpc_object_t) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_value(self.object, key, xpcDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
public func endpoint(key: String) -> xpc_endpoint_t? {
|
||||
lock.withLock {
|
||||
xpc_dictionary_get_value(self.object, key)
|
||||
|
||||
@@ -67,6 +67,7 @@ extension APIServer {
|
||||
log: log,
|
||||
routes: &routes
|
||||
)
|
||||
await containersService.setNetworksService(networkService)
|
||||
initializeHealthCheckService(log: log, routes: &routes)
|
||||
try initializeKernelService(log: log, routes: &routes)
|
||||
let volumesService = try initializeVolumeService(containersService: containersService, log: log, routes: &routes)
|
||||
@@ -269,10 +270,12 @@ extension APIServer {
|
||||
.filter { $0.isBuiltin }
|
||||
.first
|
||||
if defaultNetwork == nil {
|
||||
// FIXME: default network should be configurable elsewhere
|
||||
let config = try NetworkConfiguration(
|
||||
id: ClientNetwork.defaultNetworkName,
|
||||
mode: .nat,
|
||||
labels: [ResourceLabelKeys.role: ResourceRoleValues.builtin]
|
||||
labels: [ResourceLabelKeys.role: ResourceRoleValues.builtin],
|
||||
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")
|
||||
)
|
||||
_ = try await service.create(configuration: config)
|
||||
}
|
||||
|
||||
@@ -76,11 +76,17 @@ 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
|
||||
)
|
||||
let network = try Self.createNetwork(
|
||||
configuration: configuration,
|
||||
|
||||
@@ -60,19 +60,20 @@ extension RuntimeLinuxHelper {
|
||||
try adjustLimits()
|
||||
signal(SIGPIPE, SIG_IGN)
|
||||
|
||||
log.info("configuring XPC server")
|
||||
let interfaceStrategy: any InterfaceStrategy
|
||||
// FIXME: The network plugins that the runtime supports should be configurable elsewhere
|
||||
var interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy] = [
|
||||
NetworkPluginInfo(plugin: "container-network-vmnet", variant: "allocationOnly"): IsolatedInterfaceStrategy()
|
||||
]
|
||||
if #available(macOS 26, *) {
|
||||
interfaceStrategy = NonisolatedInterfaceStrategy(log: log)
|
||||
} else {
|
||||
interfaceStrategy = IsolatedInterfaceStrategy()
|
||||
interfaceStrategies[NetworkPluginInfo(plugin: "container-network-vmnet", variant: "reserved")] = NonisolatedInterfaceStrategy(log: log)
|
||||
}
|
||||
|
||||
log.info("configuring XPC server")
|
||||
nonisolated(unsafe) let anonymousConnection = xpc_connection_create(nil, nil)
|
||||
|
||||
let server = SandboxService(
|
||||
root: .init(fileURLWithPath: root),
|
||||
interfaceStrategy: interfaceStrategy,
|
||||
interfaceStrategies: interfaceStrategies,
|
||||
eventLoopGroup: eventLoopGroup,
|
||||
connection: anonymousConnection,
|
||||
log: log
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import ContainerResource
|
||||
import ContainerXPC
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
|
||||
@@ -81,10 +82,10 @@ extension ClientNetwork {
|
||||
|
||||
/// Delete the network with the given id.
|
||||
public static func delete(id: String) async throws {
|
||||
let client = XPCClient(service: Self.serviceIdentifier)
|
||||
let client = Self.newClient()
|
||||
let request = XPCMessage(route: .networkDelete)
|
||||
request.set(key: .networkId, value: id)
|
||||
try await client.send(request)
|
||||
let _ = try await xpcSend(client: client, message: request)
|
||||
}
|
||||
|
||||
/// Retrieve the builtin network.
|
||||
|
||||
@@ -32,6 +32,7 @@ public actor ContainersService {
|
||||
struct ContainerState {
|
||||
var snapshot: ContainerSnapshot
|
||||
var client: SandboxClient?
|
||||
var allocatedAttachments: [AllocatedAttachment]
|
||||
|
||||
func getClient() throws -> SandboxClient {
|
||||
guard let client else {
|
||||
@@ -57,6 +58,9 @@ public actor ContainersService {
|
||||
private let lock = AsyncLock()
|
||||
private var containers: [String: ContainerState]
|
||||
|
||||
// FIXME: Find a better mechanism for services running on the APIServer to work with each other
|
||||
private weak var networksService: NetworksService?
|
||||
|
||||
public init(appRoot: URL, pluginLoader: PluginLoader, log: Logger) throws {
|
||||
let containerRoot = appRoot.appendingPathComponent("containers")
|
||||
try FileManager.default.createDirectory(at: containerRoot, withIntermediateDirectories: true)
|
||||
@@ -68,6 +72,10 @@ public actor ContainersService {
|
||||
self.containers = try Self.loadAtBoot(root: containerRoot, loader: pluginLoader, log: log)
|
||||
}
|
||||
|
||||
public func setNetworksService(_ service: NetworksService) async {
|
||||
self.networksService = service
|
||||
}
|
||||
|
||||
static func loadAtBoot(root: URL, loader: PluginLoader, log: Logger) throws -> [String: ContainerState] {
|
||||
var directories = try FileManager.default.contentsOfDirectory(
|
||||
at: root,
|
||||
@@ -89,7 +97,8 @@ public actor ContainersService {
|
||||
status: .stopped,
|
||||
networks: [],
|
||||
startedDate: nil
|
||||
)
|
||||
),
|
||||
allocatedAttachments: []
|
||||
)
|
||||
results[config.id] = state
|
||||
guard runtimePlugins.first(where: { $0.name == config.runtimeHandler }) != nil else {
|
||||
@@ -282,7 +291,7 @@ public actor ContainersService {
|
||||
networks: [],
|
||||
startedDate: nil
|
||||
)
|
||||
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot), context: context)
|
||||
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot, allocatedAttachments: []), context: context)
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
@@ -305,7 +314,20 @@ public actor ContainersService {
|
||||
let path = self.containerRoot.appendingPathComponent(id)
|
||||
let config = try Self.getContainerConfiguration(at: path)
|
||||
|
||||
var allocatedAttachments = [AllocatedAttachment]()
|
||||
do {
|
||||
for n in config.networks {
|
||||
let allocatedAttach = try await self.networksService?.allocate(
|
||||
id: n.network,
|
||||
hostname: n.options.hostname,
|
||||
macAddress: n.options.macAddress
|
||||
)
|
||||
guard let allocatedAttach = allocatedAttach else {
|
||||
throw ContainerizationError(.internalError, message: "failed to allocate a network")
|
||||
}
|
||||
allocatedAttachments.append(allocatedAttach)
|
||||
}
|
||||
|
||||
try Self.registerService(
|
||||
plugin: self.runtimePlugins.first { $0.name == config.runtimeHandler }!,
|
||||
loader: self.pluginLoader,
|
||||
@@ -318,8 +340,7 @@ public actor ContainersService {
|
||||
id: id,
|
||||
runtime: runtime
|
||||
)
|
||||
|
||||
try await sandboxClient.bootstrap(stdio: stdio)
|
||||
try await sandboxClient.bootstrap(stdio: stdio, allocatedAttachments: allocatedAttachments)
|
||||
|
||||
try await self.exitMonitor.registerProcess(
|
||||
id: id,
|
||||
@@ -327,8 +348,17 @@ public actor ContainersService {
|
||||
)
|
||||
|
||||
state.client = sandboxClient
|
||||
state.allocatedAttachments = allocatedAttachments
|
||||
await self.setContainerState(id, state, context: context)
|
||||
} catch {
|
||||
for allocatedAttach in allocatedAttachments {
|
||||
do {
|
||||
try await self.networksService?.deallocate(attachment: allocatedAttach.attachment)
|
||||
} catch {
|
||||
self.log.error("failed to deallocate network attachment in \(id) for \(allocatedAttach.attachment.network): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
let label = Self.fullLaunchdServiceLabel(
|
||||
runtimeName: config.runtimeHandler,
|
||||
instanceId: id
|
||||
@@ -595,9 +625,21 @@ public actor ContainersService {
|
||||
self.log.error("Failed to deregister sandbox service for \(id): \(error)")
|
||||
}
|
||||
|
||||
// Best effort deallocate network attachments for the container. Don't throw on
|
||||
// failure so we can continue with state cleanup.
|
||||
self.log.info("Deallocating network attachments for \(id)")
|
||||
for allocatedAttach in state.allocatedAttachments {
|
||||
do {
|
||||
try await self.networksService?.deallocate(attachment: allocatedAttach.attachment)
|
||||
} catch {
|
||||
self.log.error("failed to deallocate network attachment in \(id) for \(allocatedAttach.attachment.network): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
state.snapshot.status = .stopped
|
||||
state.snapshot.networks = []
|
||||
state.client = nil
|
||||
state.allocatedAttachments = []
|
||||
await self.setContainerState(id, state, context: context)
|
||||
|
||||
let options = try getContainerCreationOptions(id: id)
|
||||
|
||||
@@ -19,6 +19,7 @@ import ContainerNetworkServiceClient
|
||||
import ContainerPersistence
|
||||
import ContainerPlugin
|
||||
import ContainerResource
|
||||
import ContainerXPC
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
@@ -27,16 +28,23 @@ import Foundation
|
||||
import Logging
|
||||
|
||||
public actor NetworksService {
|
||||
struct NetworkServiceState {
|
||||
var networkState: NetworkState
|
||||
var client: NetworkClient
|
||||
}
|
||||
|
||||
private let pluginLoader: PluginLoader
|
||||
private let resourceRoot: URL
|
||||
private let containersService: ContainersService
|
||||
private let log: Logger
|
||||
|
||||
private let store: FilesystemEntityStore<NetworkConfiguration>
|
||||
private let networkPlugin: Plugin
|
||||
private var networkStates = [String: NetworkState]()
|
||||
private let networkPlugins: [Plugin]
|
||||
private var busyNetworks = Set<String>()
|
||||
|
||||
private let stateLock = AsyncLock()
|
||||
private var serviceStates = [String: NetworkServiceState]()
|
||||
|
||||
public init(
|
||||
pluginLoader: PluginLoader,
|
||||
resourceRoot: URL,
|
||||
@@ -55,15 +63,14 @@ public actor NetworksService {
|
||||
log: log
|
||||
)
|
||||
|
||||
let networkPlugin =
|
||||
let networkPlugins =
|
||||
pluginLoader
|
||||
.findPlugins()
|
||||
.filter { $0.hasType(.network) }
|
||||
.first
|
||||
guard let networkPlugin else {
|
||||
throw ContainerizationError(.internalError, message: "cannot find network plugin")
|
||||
guard !networkPlugins.isEmpty else {
|
||||
throw ContainerizationError(.internalError, message: "cannot find any plugins with type network")
|
||||
}
|
||||
self.networkPlugin = networkPlugin
|
||||
self.networkPlugins = networkPlugins
|
||||
|
||||
let configurations = try await store.list()
|
||||
for var configuration in configurations {
|
||||
@@ -76,34 +83,55 @@ 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 configuration.pluginInfo == nil {
|
||||
configuration.pluginInfo = NetworkPluginInfo(plugin: "container-network-vmnet")
|
||||
try await store.update(configuration)
|
||||
}
|
||||
|
||||
// Start up the network.
|
||||
do {
|
||||
try await registerService(configuration: configuration)
|
||||
} catch {
|
||||
log.error(
|
||||
"failed to start network",
|
||||
"failed to start network: \(error)",
|
||||
metadata: [
|
||||
"id": "\(configuration.id)"
|
||||
])
|
||||
}
|
||||
|
||||
let client = NetworkClient(id: configuration.id)
|
||||
let networkState = try await client.state()
|
||||
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.
|
||||
var finalConfig = configuration
|
||||
switch networkState {
|
||||
case .created(_):
|
||||
networkStates[configuration.id] = NetworkState.created(configuration)
|
||||
case .running(_, let status):
|
||||
networkStates[configuration.id] = NetworkState.running(configuration, status)
|
||||
case .created(let helperConfig):
|
||||
finalConfig.pluginInfo = helperConfig.pluginInfo
|
||||
networkState = NetworkState.created(finalConfig)
|
||||
case .running(let helperConfig, let status):
|
||||
finalConfig.pluginInfo = helperConfig.pluginInfo
|
||||
networkState = NetworkState.running(finalConfig, status)
|
||||
}
|
||||
|
||||
let state = NetworkServiceState(
|
||||
networkState: networkState,
|
||||
client: client
|
||||
)
|
||||
|
||||
serviceStates[finalConfig.id] = state
|
||||
|
||||
guard case .running = networkState else {
|
||||
log.error(
|
||||
"network failed to start",
|
||||
metadata: [
|
||||
"id": "\(configuration.id)",
|
||||
"id": "\(finalConfig.id)",
|
||||
"state": "\(networkState.state)",
|
||||
])
|
||||
return
|
||||
@@ -114,8 +142,8 @@ public actor NetworksService {
|
||||
/// List all networks registered with the service.
|
||||
public func list() async throws -> [NetworkState] {
|
||||
log.info("network service: list")
|
||||
return networkStates.reduce(into: [NetworkState]()) {
|
||||
$0.append($1.value)
|
||||
return serviceStates.reduce(into: [NetworkState]()) {
|
||||
$0.append($1.value.networkState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,41 +169,45 @@ public actor NetworksService {
|
||||
defer { busyNetworks.remove(configuration.id) }
|
||||
|
||||
// Ensure the network doesn't already exist.
|
||||
guard networkStates[configuration.id] == nil else {
|
||||
throw ContainerizationError(.exists, message: "network \(configuration.id) already exists")
|
||||
}
|
||||
|
||||
// Create and start the network.
|
||||
try await registerService(configuration: configuration)
|
||||
let client = NetworkClient(id: configuration.id)
|
||||
|
||||
// Ensure the network is running, and set up the persistent network state
|
||||
// using our configuration data, as the one from the helper doesn't include
|
||||
// metadata.
|
||||
guard case .running(_, let status) = try await client.state() else {
|
||||
throw ContainerizationError(.invalidState, message: "network \(configuration.id) failed to start")
|
||||
}
|
||||
let networkState: NetworkState = .running(configuration, status)
|
||||
networkStates[configuration.id] = networkState
|
||||
|
||||
// Persist the configuration data.
|
||||
do {
|
||||
try await store.create(configuration)
|
||||
return networkState
|
||||
} catch {
|
||||
networkStates.removeValue(forKey: configuration.id)
|
||||
do {
|
||||
try pluginLoader.deregisterWithLaunchd(plugin: networkPlugin, instanceId: configuration.id)
|
||||
} catch {
|
||||
log.error(
|
||||
"failed to deregister network service after failed creation",
|
||||
metadata: [
|
||||
"id": "\(configuration.id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
return try await self.stateLock.withLock { _ in
|
||||
guard await self.serviceStates[configuration.id] == nil else {
|
||||
throw ContainerizationError(.exists, message: "network \(configuration.id) already exists")
|
||||
}
|
||||
|
||||
throw error
|
||||
// Create and start the network.
|
||||
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")
|
||||
}
|
||||
var finalConfig = configuration
|
||||
finalConfig.pluginInfo = helperConfig.pluginInfo
|
||||
|
||||
let networkState: NetworkState = .running(finalConfig, status)
|
||||
let serviceState = NetworkServiceState(networkState: networkState, client: client)
|
||||
await self.setServiceState(key: finalConfig.id, value: serviceState)
|
||||
|
||||
// Persist the configuration data.
|
||||
do {
|
||||
try await self.store.create(finalConfig)
|
||||
return networkState
|
||||
} catch {
|
||||
await self.removeServiceState(key: finalConfig.id)
|
||||
do {
|
||||
try await self.deregisterService(configuration: finalConfig)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to deregister network service after failed creation",
|
||||
metadata: [
|
||||
"id": "\(finalConfig.id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,86 +228,117 @@ public actor NetworksService {
|
||||
"id": "\(id)"
|
||||
])
|
||||
|
||||
guard let networkState = networkStates[id] else {
|
||||
throw ContainerizationError(.notFound, message: "no network for id \(id)")
|
||||
}
|
||||
try await stateLock.withLock { _ in
|
||||
guard let serviceState = await self.serviceStates[id] else {
|
||||
throw ContainerizationError(.notFound, message: "no network for id \(id)")
|
||||
}
|
||||
|
||||
// basic sanity checks on network itself
|
||||
if networkState.isBuiltin {
|
||||
throw ContainerizationError(.invalidArgument, message: "cannot delete builtin network: \(id)")
|
||||
}
|
||||
guard case .running(let netConfig, _) = serviceState.networkState else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete network \(id) in state \(serviceState.networkState.state)")
|
||||
}
|
||||
|
||||
guard case .running = networkState else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete network \(id) in state \(networkState.state)")
|
||||
}
|
||||
// basic sanity checks on network itself
|
||||
if serviceState.networkState.isBuiltin {
|
||||
throw ContainerizationError(.invalidArgument, message: "cannot delete builtin network: \(id)")
|
||||
}
|
||||
|
||||
// prevent container operations while we atomically check and delete
|
||||
try await containersService.withContainerList { containers in
|
||||
// find all containers that refer to the network
|
||||
var referringContainers = Set<String>()
|
||||
for container in containers {
|
||||
for attachmentConfiguration in container.configuration.networks {
|
||||
if attachmentConfiguration.network == id {
|
||||
referringContainers.insert(container.configuration.id)
|
||||
break
|
||||
// prevent container operations while we atomically check and delete
|
||||
try await self.containersService.withContainerList { containers in
|
||||
// find all containers that refer to the network
|
||||
var referringContainers = Set<String>()
|
||||
for container in containers {
|
||||
for attachmentConfiguration in container.configuration.networks {
|
||||
if attachmentConfiguration.network == id {
|
||||
referringContainers.insert(container.configuration.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bail if any referring containers
|
||||
guard referringContainers.isEmpty else {
|
||||
throw ContainerizationError(
|
||||
.invalidState,
|
||||
message: "cannot delete subnet \(id) with referring containers: \(referringContainers.joined(separator: ", "))"
|
||||
)
|
||||
}
|
||||
|
||||
// disable the allocator so nothing else can attach
|
||||
// TODO: remove this from the network helper later, not necesssary now that withContainerList is here
|
||||
guard try await serviceState.client.disableAllocator() else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete subnet \(id) because the IP allocator cannot be disabled with active containers")
|
||||
}
|
||||
|
||||
// start network deletion, this is the last place we'll want to throw
|
||||
do {
|
||||
try await self.deregisterService(configuration: netConfig)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to deregister network service",
|
||||
metadata: [
|
||||
"id": "\(id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
}
|
||||
|
||||
// deletion is underway, do not throw anything now
|
||||
do {
|
||||
try await self.store.delete(id)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to delete network from configuration store",
|
||||
metadata: [
|
||||
"id": "\(id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// bail if any referring containers
|
||||
guard referringContainers.isEmpty else {
|
||||
throw ContainerizationError(
|
||||
.invalidState,
|
||||
message: "cannot delete subnet \(id) with referring containers: \(referringContainers.joined(separator: ", "))"
|
||||
)
|
||||
}
|
||||
|
||||
// disable the allocator so nothing else can attach
|
||||
// TODO: remove this from the network helper later, not necesssary now that withContainerList is here
|
||||
let client = NetworkClient(id: id)
|
||||
guard try await client.disableAllocator() else {
|
||||
throw ContainerizationError(.invalidState, message: "cannot delete subnet \(id) because the IP allocator cannot be disabled with active containers")
|
||||
}
|
||||
|
||||
// start network deletion, this is the last place we'll want to throw
|
||||
do {
|
||||
try self.pluginLoader.deregisterWithLaunchd(plugin: self.networkPlugin, instanceId: id)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to deregister network service",
|
||||
metadata: [
|
||||
"id": "\(id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
}
|
||||
|
||||
// deletion is underway, do not throw anything now
|
||||
do {
|
||||
try await self.store.delete(id)
|
||||
} catch {
|
||||
self.log.error(
|
||||
"failed to delete network from configuration store",
|
||||
metadata: [
|
||||
"id": "\(id)",
|
||||
"error": "\(error.localizedDescription)",
|
||||
])
|
||||
}
|
||||
// having deleted successfully, remove the runtime state
|
||||
await self.removeServiceState(key: id)
|
||||
}
|
||||
|
||||
// having deleted successfully, remove the runtime state
|
||||
self.networkStates.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
/// Perform a hostname lookup on all networks.
|
||||
public func lookup(hostname: String) async throws -> Attachment? {
|
||||
for id in networkStates.keys {
|
||||
let client = NetworkClient(id: id)
|
||||
guard let allocation = try await client.lookup(hostname: hostname) else {
|
||||
continue
|
||||
try await self.stateLock.withLock { _ in
|
||||
for state in await self.serviceStates.values {
|
||||
guard let allocation = try await state.client.lookup(hostname: hostname) else {
|
||||
continue
|
||||
}
|
||||
return allocation
|
||||
}
|
||||
return allocation
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func allocate(id: String, hostname: String, macAddress: MACAddress?) async throws -> AllocatedAttachment {
|
||||
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")
|
||||
}
|
||||
let (attach, additionalData) = try await serviceState.client.allocate(hostname: hostname, macAddress: macAddress)
|
||||
return AllocatedAttachment(
|
||||
attachment: attach,
|
||||
additionalData: additionalData,
|
||||
pluginInfo: pluginInfo
|
||||
)
|
||||
}
|
||||
|
||||
public func deallocate(attachment: Attachment) async throws {
|
||||
guard let serviceState = serviceStates[attachment.network] else {
|
||||
throw ContainerizationError(.notFound, message: "no network for id \(attachment.network)")
|
||||
}
|
||||
return try await serviceState.client.deallocate(hostname: attachment.hostname)
|
||||
}
|
||||
|
||||
private static func getClient(configuration: NetworkConfiguration) throws -> 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)
|
||||
}
|
||||
|
||||
private func registerService(configuration: NetworkConfiguration) async throws {
|
||||
@@ -283,6 +346,17 @@ 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 {
|
||||
throw ContainerizationError(
|
||||
.notFound,
|
||||
message: "unable to locate network plugin \(pluginInfo.plugin)"
|
||||
)
|
||||
}
|
||||
|
||||
guard let serviceIdentifier = networkPlugin.getMachService(instanceId: configuration.id, type: .network) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "unsupported network mode \(configuration.mode.rawValue)")
|
||||
}
|
||||
@@ -298,8 +372,8 @@ public actor NetworksService {
|
||||
|
||||
if let ipv4Subnet = configuration.ipv4Subnet {
|
||||
var existingCidrs: [CIDRv4] = []
|
||||
for networkState in networkStates.values {
|
||||
if case .running(_, let status) = networkState {
|
||||
for serviceState in serviceStates.values {
|
||||
if case .running(_, let status) = serviceState.networkState {
|
||||
existingCidrs.append(status.ipv4Subnet)
|
||||
}
|
||||
}
|
||||
@@ -318,8 +392,8 @@ public actor NetworksService {
|
||||
|
||||
if let ipv6Subnet = configuration.ipv6Subnet {
|
||||
var existingCidrs: [CIDRv6] = []
|
||||
for networkState in networkStates.values {
|
||||
if case .running(_, let status) = networkState, let otherIPv6Subnet = status.ipv6Subnet {
|
||||
for serviceState in serviceStates.values {
|
||||
if case .running(_, let status) = serviceState.networkState, let otherIPv6Subnet = status.ipv6Subnet {
|
||||
existingCidrs.append(otherIPv6Subnet)
|
||||
}
|
||||
}
|
||||
@@ -336,6 +410,10 @@ public actor NetworksService {
|
||||
args += ["--subnet-v6", ipv6Subnet.description]
|
||||
}
|
||||
|
||||
if let variant = configuration.pluginInfo?.variant {
|
||||
args += ["--variant", variant]
|
||||
}
|
||||
|
||||
try await pluginLoader.registerWithLaunchd(
|
||||
plugin: networkPlugin,
|
||||
pluginStateRoot: store.entityUrl(configuration.id),
|
||||
@@ -343,4 +421,27 @@ public actor NetworksService {
|
||||
instanceId: configuration.id
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
throw ContainerizationError(
|
||||
.notFound,
|
||||
message: "unable to locate network plugin \(pluginInfo.plugin)"
|
||||
)
|
||||
}
|
||||
try self.pluginLoader.deregisterWithLaunchd(plugin: networkPlugin, instanceId: configuration.id)
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworksService {
|
||||
private func removeServiceState(key: String) {
|
||||
self.serviceStates.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
private func setServiceState(key: String, value: NetworkServiceState) {
|
||||
self.serviceStates[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,18 +22,23 @@ import Foundation
|
||||
|
||||
/// A client for interacting with a single network.
|
||||
public struct NetworkClient: Sendable {
|
||||
// FIXME: need more flexibility than a hard-coded constant?
|
||||
static let label = "com.apple.container.network.container-network-vmnet"
|
||||
static let label = "com.apple.container.network"
|
||||
|
||||
public static func machServiceLabel(id: String, plugin: String) -> String {
|
||||
"\(Self.label).\(plugin).\(id)"
|
||||
}
|
||||
|
||||
private var machServiceLabel: String {
|
||||
"\(Self.label).\(id)"
|
||||
Self.machServiceLabel(id: id, plugin: plugin)
|
||||
}
|
||||
|
||||
let id: String
|
||||
let plugin: String
|
||||
|
||||
/// Create a client for a network.
|
||||
public init(id: String) {
|
||||
public init(id: String, plugin: String) {
|
||||
self.id = id
|
||||
self.plugin = plugin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import ContainerXPC
|
||||
import Containerization
|
||||
@@ -76,7 +77,7 @@ public struct SandboxClient: Sendable {
|
||||
|
||||
// Runtime Methods
|
||||
extension SandboxClient {
|
||||
public func bootstrap(stdio: [FileHandle?]) async throws {
|
||||
public func bootstrap(stdio: [FileHandle?], allocatedAttachments: [AllocatedAttachment]) async throws {
|
||||
let request = XPCMessage(route: SandboxRoutes.bootstrap.rawValue)
|
||||
|
||||
for (i, h) in stdio.enumerated() {
|
||||
@@ -96,6 +97,7 @@ extension SandboxClient {
|
||||
}
|
||||
|
||||
do {
|
||||
try request.setAllocatedAttachments(allocatedAttachments)
|
||||
try await self.client.send(request)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
@@ -322,4 +324,26 @@ extension XPCMessage {
|
||||
}
|
||||
return try JSONDecoder().decode(SandboxSnapshot.self, from: data)
|
||||
}
|
||||
|
||||
func setAllocatedAttachments(_ allocatedAttachments: [AllocatedAttachment]) throws {
|
||||
let encoder = JSONEncoder()
|
||||
let allocatedAttachmentsArray = xpc_array_create_empty()
|
||||
for allocatedAttach in allocatedAttachments {
|
||||
let xpcObject: xpc_object_t = xpc_dictionary_create_empty()
|
||||
let networkXPC = XPCMessage(object: xpcObject)
|
||||
|
||||
let attachmentEncoded = try encoder.encode(allocatedAttach.attachment)
|
||||
networkXPC.set(key: SandboxKeys.networkAttachment.rawValue, value: attachmentEncoded)
|
||||
|
||||
let pluginInfoEncoded = try encoder.encode(allocatedAttach.pluginInfo)
|
||||
networkXPC.set(key: SandboxKeys.networkPluginInfo.rawValue, value: pluginInfoEncoded)
|
||||
|
||||
if let additionalData = allocatedAttach.additionalData {
|
||||
xpc_dictionary_set_value(networkXPC.underlying, SandboxKeys.networkAdditionalData.rawValue, additionalData.underlying)
|
||||
}
|
||||
|
||||
xpc_array_append_value(allocatedAttachmentsArray, networkXPC.underlying)
|
||||
}
|
||||
self.set(key: SandboxKeys.allocatedAttachments.rawValue, value: allocatedAttachmentsArray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,10 @@ public enum SandboxKeys: String {
|
||||
|
||||
/// Container statistics
|
||||
case statistics
|
||||
|
||||
/// Network resource keys.
|
||||
case allocatedAttachments
|
||||
case networkAdditionalData
|
||||
case networkAttachment
|
||||
case networkPluginInfo
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerNetworkServiceClient
|
||||
import ContainerAPIClient
|
||||
import ContainerPersistence
|
||||
import ContainerResource
|
||||
import ContainerSandboxServiceClient
|
||||
@@ -39,7 +39,7 @@ import struct ContainerizationOCI.Process
|
||||
public actor SandboxService {
|
||||
private let connection: xpc_connection_t
|
||||
private let root: URL
|
||||
private let interfaceStrategy: InterfaceStrategy
|
||||
private let interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy]
|
||||
private var container: ContainerInfo?
|
||||
private let monitor: ExitMonitor
|
||||
private let eventLoopGroup: any EventLoopGroup
|
||||
@@ -62,13 +62,13 @@ public actor SandboxService {
|
||||
|
||||
public init(
|
||||
root: URL,
|
||||
interfaceStrategy: InterfaceStrategy,
|
||||
interfaceStrategies: [NetworkPluginInfo: InterfaceStrategy],
|
||||
eventLoopGroup: any EventLoopGroup,
|
||||
connection: xpc_connection_t,
|
||||
log: Logger
|
||||
) {
|
||||
self.root = root
|
||||
self.interfaceStrategy = interfaceStrategy
|
||||
self.interfaceStrategies = interfaceStrategies
|
||||
self.log = log
|
||||
self.monitor = ExitMonitor(log: log)
|
||||
self.eventLoopGroup = eventLoopGroup
|
||||
@@ -130,9 +130,11 @@ public actor SandboxService {
|
||||
logger: self.log
|
||||
)
|
||||
|
||||
let allocatedAttachments = try message.getAllocatedAttachments()
|
||||
|
||||
// Dynamically configure the DNS nameserver from a network if no explicit configuration
|
||||
if let dns = config.dns, dns.nameservers.isEmpty {
|
||||
let defaultNameservers = try await self.getDefaultNameservers(attachmentConfigurations: config.networks)
|
||||
let defaultNameservers = try await self.getDefaultNameservers(allocatedAttachments: allocatedAttachments)
|
||||
if !defaultNameservers.isEmpty {
|
||||
config.dns = ContainerConfiguration.DNSConfiguration(
|
||||
nameservers: defaultNameservers,
|
||||
@@ -145,16 +147,19 @@ public actor SandboxService {
|
||||
|
||||
var attachments: [Attachment] = []
|
||||
var interfaces: [Interface] = []
|
||||
for index in 0..<config.networks.count {
|
||||
let network = config.networks[index]
|
||||
let client = NetworkClient(id: network.network)
|
||||
let (attachment, additionalData) = try await client.allocate(hostname: network.options.hostname, macAddress: network.options.macAddress)
|
||||
attachments.append(attachment)
|
||||
for index in 0..<allocatedAttachments.count {
|
||||
let allocatedAttach = allocatedAttachments[index]
|
||||
attachments.append(allocatedAttach.attachment)
|
||||
|
||||
let interface = try self.interfaceStrategy.toInterface(
|
||||
attachment: attachment,
|
||||
guard let iStrategy = self.interfaceStrategies[allocatedAttach.pluginInfo] else {
|
||||
throw ContainerizationError(
|
||||
.internalError, message: "no available interface strategy for network \(allocatedAttach.attachment.network), \(allocatedAttach.pluginInfo)")
|
||||
}
|
||||
|
||||
let interface = try iStrategy.toInterface(
|
||||
attachment: allocatedAttach.attachment,
|
||||
interfaceIndex: index,
|
||||
additionalData: additionalData
|
||||
additionalData: allocatedAttach.additionalData
|
||||
)
|
||||
interfaces.append(interface)
|
||||
}
|
||||
@@ -867,10 +872,9 @@ public actor SandboxService {
|
||||
try Self.configureInitialProcess(czConfig: &czConfig, config: config)
|
||||
}
|
||||
|
||||
private func getDefaultNameservers(attachmentConfigurations: [AttachmentConfiguration]) async throws -> [String] {
|
||||
for attachmentConfiguration in attachmentConfigurations {
|
||||
let client = NetworkClient(id: attachmentConfiguration.network)
|
||||
let state = try await client.state()
|
||||
private func getDefaultNameservers(allocatedAttachments: [AllocatedAttachment]) async throws -> [String] {
|
||||
for allocatedAttach in allocatedAttachments {
|
||||
let state = try await ClientNetwork.get(id: allocatedAttach.attachment.network)
|
||||
guard case .running(_, let status) = state else {
|
||||
continue
|
||||
}
|
||||
@@ -1035,16 +1039,7 @@ public actor SandboxService {
|
||||
self.log.error("failed to stop container during cleanup: \(error)")
|
||||
}
|
||||
|
||||
// Give back our lovely IP(s)
|
||||
await self.stopSocketForwarders()
|
||||
for attachment in containerInfo.attachments {
|
||||
let client = NetworkClient(id: attachment.network)
|
||||
do {
|
||||
try await client.deallocate(hostname: attachment.hostname)
|
||||
} catch {
|
||||
self.log.error("failed to deallocate hostname \(attachment.hostname) on network \(attachment.network) during cleanup: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
let status = exitStatus ?? ExitStatus(exitCode: 255)
|
||||
let waiters = self.waiters[id] ?? []
|
||||
@@ -1096,6 +1091,50 @@ extension XPCMessage {
|
||||
}
|
||||
return try JSONDecoder().decode(ProcessConfiguration.self, from: data)
|
||||
}
|
||||
|
||||
fileprivate func getAllocatedAttachments() throws -> [AllocatedAttachment] {
|
||||
guard let attachmentArray = xpc_dictionary_get_value(self.underlying, SandboxKeys.allocatedAttachments.rawValue) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "missing allocatedAttachments array in message")
|
||||
}
|
||||
|
||||
var results = [AllocatedAttachment]()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let arrayCount = xpc_array_get_count(attachmentArray)
|
||||
|
||||
for i in 0..<arrayCount {
|
||||
guard let allocatedAttach = xpc_array_get_dictionary(attachmentArray, i) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid allocated attachment at index \(i)")
|
||||
}
|
||||
|
||||
let allocatedAttachXPC = XPCMessage(object: allocatedAttach)
|
||||
|
||||
let attachmentData = allocatedAttachXPC.dataNoCopy(key: SandboxKeys.networkAttachment.rawValue)
|
||||
let pluginInfoData = allocatedAttachXPC.dataNoCopy(key: SandboxKeys.networkPluginInfo.rawValue)
|
||||
|
||||
guard let attachmentData = attachmentData, let pluginInfoData = pluginInfoData else {
|
||||
throw ContainerizationError(.invalidArgument, message: "must have attachment and plugin information for network")
|
||||
}
|
||||
|
||||
let attachment = try decoder.decode(Attachment.self, from: attachmentData)
|
||||
let pluginInfo = try decoder.decode(NetworkPluginInfo.self, from: pluginInfoData)
|
||||
|
||||
let additionalDataXPC: XPCMessage? = {
|
||||
if let rawData = xpc_dictionary_get_dictionary(allocatedAttachXPC.underlying, SandboxKeys.networkAdditionalData.rawValue) {
|
||||
return XPCMessage(object: rawData)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
results.append(
|
||||
AllocatedAttachment(
|
||||
attachment: attachment,
|
||||
additionalData: additionalDataXPC,
|
||||
pluginInfo: pluginInfo
|
||||
))
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
extension ContainerResource.Bundle {
|
||||
|
||||
@@ -21,9 +21,15 @@ 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)
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testValidationGoodId() throws {
|
||||
@@ -38,7 +44,13 @@ struct NetworkConfigurationTest {
|
||||
"foo": "bar",
|
||||
"baz": String(repeating: "0", count: 4096 - "baz".count - "=".count),
|
||||
]
|
||||
_ = try NetworkConfiguration(id: id, mode: .nat, ipv4Subnet: ipv4Subnet, labels: labels)
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +68,13 @@ struct NetworkConfigurationTest {
|
||||
"baz": String(repeating: "0", count: 4096 - "baz".count - "=".count),
|
||||
]
|
||||
#expect {
|
||||
_ = try NetworkConfiguration(id: id, mode: .nat, ipv4Subnet: ipv4Subnet, labels: labels)
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
)
|
||||
} throws: { error in
|
||||
guard let err = error as? ContainerizationError else { return false }
|
||||
#expect(err.code == .invalidArgument)
|
||||
@@ -76,7 +94,13 @@ struct NetworkConfigurationTest {
|
||||
for labels in allLabels {
|
||||
let id = "foo"
|
||||
let ipv4Subnet = try CIDRv4("192.168.64.1/24")
|
||||
_ = try NetworkConfiguration(id: id, mode: .nat, ipv4Subnet: ipv4Subnet, labels: labels)
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +116,13 @@ struct NetworkConfigurationTest {
|
||||
let id = "foo"
|
||||
let ipv4Subnet = try CIDRv4("192.168.64.1/24")
|
||||
#expect {
|
||||
_ = try NetworkConfiguration(id: id, mode: .nat, ipv4Subnet: ipv4Subnet, labels: labels)
|
||||
_ = try NetworkConfiguration(
|
||||
id: id,
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
labels: labels,
|
||||
pluginInfo: defaultNetworkPluginInfo
|
||||
)
|
||||
} throws: { error in
|
||||
guard let err = error as? ContainerizationError else { return false }
|
||||
#expect(err.code == .invalidArgument)
|
||||
|
||||
Reference in New Issue
Block a user