API: Rename to NetworkClient, client as instance. (#1405)

- Part of #1404.
- COMPATIBILITY: Breaking change to client API. XPC protocol and
persistent data remain fully compatibile.
- Renames `ClientNetwork` to `NetworkClient`, aligning with
`ContainerClient`.
- Client uses instance methods instead of utility functions, with an
`init(serviceIdentifier:)` for the XPC service name. The value is
currently unused.
This commit is contained in:
J Logan authored and GitHub committed 2026-04-08 13:41:12 -07:00
1 parent a557ce80a5
commit a1981c30ac
12 files changed
+176 -116

No files matched your search

+1 -1
View File
@@ -314,7 +314,7 @@ extension APIServer {
if defaultNetwork == nil {
// FIXME: default network should be configurable elsewhere
let config = try NetworkConfiguration(
id: ClientNetwork.defaultNetworkName,
id: NetworkClient.defaultNetworkName,
mode: .nat,
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")
@@ -265,7 +265,8 @@ extension Application {
// Enable Rosetta only if the user didn't ask to disable it
config.rosetta = useRosetta
guard let defaultNetwork = try await ClientNetwork.builtin else {
let networkClient = NetworkClient()
guard let defaultNetwork = try await networkClient.builtin else {
throw ContainerizationError(.invalidState, message: "default network is not present")
}
guard case .running(_, _) = defaultNetwork else {
@@ -73,7 +73,8 @@ extension Application {
labels: parsedLabels,
pluginInfo: NetworkPluginInfo(plugin: self.plugin, variant: self.pluginVariant)
)
let state = try await ClientNetwork.create(configuration: config)
let networkClient = NetworkClient()
let state = try await networkClient.create(configuration: config)
print(state.id)
}
}
@@ -51,14 +51,15 @@ extension Application {
}
public mutating func run() async throws {
let networkClient = NetworkClient()
let uniqueNetworkNames = Set<String>(networkNames)
let networks: [NetworkState]
if all {
networks = try await ClientNetwork.list()
networks = try await networkClient.list()
.filter { !$0.isBuiltin }
} else {
networks = try await ClientNetwork.list()
networks = try await networkClient.list()
.filter { c in
guard uniqueNetworkNames.contains(c.id) else {
return false
@@ -96,7 +97,7 @@ extension Application {
do {
// Delete atomically disables the IP allocator, then deletes
// the allocator. The disable fails if any IPs are still in use.
try await ClientNetwork.delete(id: network.id)
try await networkClient.delete(id: network.id)
print(network.id)
return nil
} catch {
@@ -34,7 +34,8 @@ extension Application {
public init() {}
public func run() async throws {
let items = try await ClientNetwork.list().filter {
let networkClient = NetworkClient()
let items = try await networkClient.list().filter {
networks.contains($0.id)
}.map {
PrintableNetwork($0)
@@ -40,7 +40,8 @@ extension Application {
public init() {}
public func run() async throws {
let networks = try await ClientNetwork.list()
let networkClient = NetworkClient()
let networks = try await networkClient.list()
let items = networks.map { PrintableNetwork($0) }
try Output.render(json: items, display: items, format: format, quiet: quiet)
}
@@ -30,9 +30,10 @@ extension Application.NetworkCommand {
public var logOptions: Flags.Logging
public func run() async throws {
let networkClient = NetworkClient()
let client = ContainerClient()
let allContainers = try await client.list()
let allNetworks = try await ClientNetwork.list()
let allNetworks = try await networkClient.list()
var networksInUse = Set<String>()
for container in allContainers {
@@ -49,7 +50,7 @@ extension Application.NetworkCommand {
for network in networksToPrune {
do {
try await ClientNetwork.delete(id: network.id)
try await networkClient.delete(id: network.id)
prunedNetworks.append(network.id)
} catch {
// Note: This failure may occur due to a race condition between the network/
@@ -1,97 +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 ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import ContainerizationOS
import Foundation
public struct ClientNetwork {
static let serviceIdentifier = "com.apple.container.apiserver"
public static let defaultNetworkName = "default"
public static let noNetworkName = "none"
}
extension ClientNetwork {
private static func newClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}
private static func xpcSend(
client: XPCClient,
message: XPCMessage,
timeout: Duration? = XPCClient.xpcRegistrationTimeout
) async throws -> XPCMessage {
try await client.send(message, responseTimeout: timeout)
}
public static func create(configuration: NetworkConfiguration) async throws -> NetworkState {
let client = Self.newClient()
let request = XPCMessage(route: .networkCreate)
request.set(key: .networkId, value: configuration.id)
let data = try JSONEncoder().encode(configuration)
request.set(key: .networkConfig, value: data)
let response = try await xpcSend(client: client, message: request)
let responseData = response.dataNoCopy(key: .networkState)
guard let responseData else {
throw ContainerizationError(.invalidArgument, message: "network configuration not received")
}
let state = try JSONDecoder().decode(NetworkState.self, from: responseData)
return state
}
public static func list() async throws -> [NetworkState] {
let client = Self.newClient()
let request = XPCMessage(route: .networkList)
let response = try await xpcSend(client: client, message: request, timeout: .seconds(1))
let responseData = response.dataNoCopy(key: .networkStates)
guard let responseData else {
return []
}
let states = try JSONDecoder().decode([NetworkState].self, from: responseData)
return states
}
/// Get the network for the provided id.
public static func get(id: String) async throws -> NetworkState {
let networks = try await list()
guard let network = networks.first(where: { $0.id == id }) else {
throw ContainerizationError(.notFound, message: "network \(id) not found")
}
return network
}
/// Delete the network with the given id.
public static func delete(id: String) async throws {
let client = Self.newClient()
let request = XPCMessage(route: .networkDelete)
request.set(key: .networkId, value: id)
let _ = try await xpcSend(client: client, message: request)
}
/// Retrieve the builtin network.
public static var builtin: NetworkState? {
get async throws {
try await list().first { $0.isBuiltin }
}
}
}
@@ -0,0 +1,149 @@
//===----------------------------------------------------------------------===//
// 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 ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationExtras
import ContainerizationOS
import Foundation
/// A client for managing virtual networks through the container API server.
///
/// `NetworkClient` communicates with `container-apiserver` over XPC to create,
/// list, inspect, and delete networks. Each instance holds a dedicated XPC
/// connection; create one client and reuse it across related operations.
///
/// ```swift
/// let client = NetworkClient()
/// let state = try await client.create(configuration: config)
/// let networks = try await client.list()
/// try await client.delete(id: state.id)
/// ```
public struct NetworkClient: Sendable {
/// The Mach service name used to locate the container API server.
///
/// Pass a different value to ``init(serviceIdentifier:)`` to connect to an
/// alternative service endpoint, for example during testing.
public static let defaultServiceIdentifier = "com.apple.container.apiserver"
/// The name of the default network created automatically on first use.
public static let defaultNetworkName = "default"
/// The reserved name that indicates a container should have no network attachment.
public static let noNetworkName = "none"
private let xpcClient: XPCClient
/// Creates a new network client connected to the given service endpoint.
///
/// - Parameter serviceIdentifier: The Mach service name of the API server.
/// Defaults to ``defaultServiceIdentifier``.
public init(serviceIdentifier: String = Self.defaultServiceIdentifier) {
self.xpcClient = XPCClient(service: serviceIdentifier)
}
@discardableResult
private func xpcSend(
message: XPCMessage,
timeout: Duration? = XPCClient.xpcRegistrationTimeout
) async throws -> XPCMessage {
try await xpcClient.send(message, responseTimeout: timeout)
}
/// Creates a new network with the given configuration.
///
/// The API server launches a network plugin instance for the new network and
/// returns a ``NetworkState`` reflecting the network once it is running.
///
/// - Parameter configuration: The configuration describing the network to create.
/// - Returns: The running state of the newly created network.
/// - Throws: ``ContainerizationError`` if the server does not return a valid
/// network state, or if the underlying XPC call fails.
public func create(configuration: NetworkConfiguration) async throws -> NetworkState {
let request = XPCMessage(route: .networkCreate)
request.set(key: .networkId, value: configuration.id)
let data = try JSONEncoder().encode(configuration)
request.set(key: .networkConfig, value: data)
let response = try await xpcSend(message: request)
let responseData = response.dataNoCopy(key: .networkState)
guard let responseData else {
throw ContainerizationError(.invalidArgument, message: "network configuration not received")
}
let state = try JSONDecoder().decode(NetworkState.self, from: responseData)
return state
}
/// Returns the current state of all networks known to the API server.
///
/// - Returns: An array of ``NetworkState`` values, or an empty array if no
/// networks exist or the server returns no data.
/// - Throws: ``ContainerizationError`` if the underlying XPC call fails.
public func list() async throws -> [NetworkState] {
let request = XPCMessage(route: .networkList)
let response = try await xpcSend(message: request, timeout: .seconds(1))
let responseData = response.dataNoCopy(key: .networkStates)
guard let responseData else {
return []
}
let states = try JSONDecoder().decode([NetworkState].self, from: responseData)
return states
}
/// Returns the network with the given identifier.
///
/// - Parameter id: The identifier of the network to look up.
/// - Returns: The ``NetworkState`` for the matching network.
/// - Throws: ``ContainerizationError/notFound`` if no network with the given
/// identifier exists, or a communication error if the XPC call fails.
public func get(id: String) async throws -> NetworkState {
let networks = try await list()
guard let network = networks.first(where: { $0.id == id }) else {
throw ContainerizationError(.notFound, message: "network \(id) not found")
}
return network
}
/// Deletes the network with the given identifier.
///
/// Deletion succeeds only when no containers are currently attached to the
/// network. The default network cannot be deleted.
///
/// - Parameter id: The identifier of the network to delete.
/// - Throws: ``ContainerizationError`` if the network has active attachments,
/// if the network is the built-in default, or if the XPC call fails.
public func delete(id: String) async throws {
let request = XPCMessage(route: .networkDelete)
request.set(key: .networkId, value: id)
try await xpcSend(message: request)
}
/// The built-in network, if one exists.
///
/// The built-in network is created automatically on first use and cannot be
/// deleted. Returns `nil` if the API server cannot find a network with the
/// built-in resource labels.
///
/// - Throws: ``ContainerizationError`` if the underlying XPC call fails.
public var builtin: NetworkState? {
get async throws {
try await list().first { $0.isBuiltin }
}
}
}
@@ -195,20 +195,21 @@ public struct Utility {
// Parse network specifications with properties
let parsedNetworks = try management.networks.map { try Parser.network($0) }
if management.networks.contains(ClientNetwork.noNetworkName) {
if management.networks.contains(NetworkClient.noNetworkName) {
guard management.networks.count == 1 else {
throw ContainerizationError(.unsupported, message: "no other networks may be created along with network \(ClientNetwork.noNetworkName)")
throw ContainerizationError(.unsupported, message: "no other networks may be created along with network \(NetworkClient.noNetworkName)")
}
config.networks = []
} else {
let builtinNetworkId = try await ClientNetwork.builtin?.id
let networkClient = NetworkClient()
let builtinNetworkId = try await networkClient.builtin?.id
config.networks = try getAttachmentConfigurations(
containerId: config.id,
builtinNetworkId: builtinNetworkId,
networks: parsedNetworks
)
for attachmentConfiguration in config.networks {
let network: NetworkState = try await ClientNetwork.get(id: attachmentConfiguration.network)
let network: NetworkState = try await networkClient.get(id: attachmentConfiguration.network)
guard case .running(_, _) = network else {
throw ContainerizationError(.invalidState, message: "network \(attachmentConfiguration.network) is not running")
}
@@ -30,7 +30,7 @@ import Logging
public actor NetworksService {
struct NetworkServiceState {
var networkState: NetworkState
var client: NetworkClient
var client: ContainerNetworkServiceClient.NetworkClient
}
private let pluginLoader: PluginLoader
@@ -79,7 +79,7 @@ public actor NetworksService {
for configuration in configurations {
// Ensure the network with id "default" is marked as builtin.
var updatedLabels: [String: String]?
if configuration.id == ClientNetwork.defaultNetworkName {
if configuration.id == NetworkClient.defaultNetworkName {
let role = configuration.labels[ResourceLabelKeys.role]
if role == nil || role != ResourceRoleValues.builtin {
var labels = configuration.labels.dictionary
@@ -201,7 +201,7 @@ public actor NetworksService {
}
//Ensure that the network is not named "none"
if configuration.id == ClientNetwork.noNetworkName {
if configuration.id == NetworkClient.noNetworkName {
throw ContainerizationError(.unsupported, message: "network \(configuration.id) is not a valid name")
}
@@ -400,7 +400,7 @@ public actor NetworksService {
return try await serviceState.client.deallocate(hostname: attachment.hostname)
}
private static func getClient(configuration: NetworkConfiguration) throws -> NetworkClient {
private static func getClient(configuration: NetworkConfiguration) throws -> ContainerNetworkServiceClient.NetworkClient {
guard let pluginInfo = configuration.pluginInfo else {
throw ContainerizationError(.internalError, message: "network \(configuration.id) missing plugin information")
}
@@ -900,8 +900,9 @@ public actor SandboxService {
}
private func getDefaultNameservers(allocatedAttachments: [AllocatedAttachment]) async throws -> [String] {
let networkClient = NetworkClient()
for allocatedAttach in allocatedAttachments {
let state = try await ClientNetwork.get(id: allocatedAttach.attachment.network)
let state = try await networkClient.get(id: allocatedAttach.attachment.network)
guard case .running(_, let status) = state else {
continue
}