mirror of
https://github.com/apple/container.git
synced 2026-11-05 21:43:24 +00:00
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:
1 parent
a557ce80a5
commit
a1981c30ac
12 files changed
+176
-116
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user