Skip to content

Commit a1981c3

Browse files
authored
API: Rename to NetworkClient, client as instance. (apple#1405)
- Part of apple#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.
1 parent a557ce8 commit a1981c3

12 files changed

Lines changed: 176 additions & 116 deletions

File tree

Sources/APIServer/APIServer+Start.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ extension APIServer {
314314
if defaultNetwork == nil {
315315
// FIXME: default network should be configurable elsewhere
316316
let config = try NetworkConfiguration(
317-
id: ClientNetwork.defaultNetworkName,
317+
id: NetworkClient.defaultNetworkName,
318318
mode: .nat,
319319
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
320320
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")

Sources/ContainerCommands/Builder/BuilderStart.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ extension Application {
265265
// Enable Rosetta only if the user didn't ask to disable it
266266
config.rosetta = useRosetta
267267

268-
guard let defaultNetwork = try await ClientNetwork.builtin else {
268+
let networkClient = NetworkClient()
269+
guard let defaultNetwork = try await networkClient.builtin else {
269270
throw ContainerizationError(.invalidState, message: "default network is not present")
270271
}
271272
guard case .running(_, _) = defaultNetwork else {

Sources/ContainerCommands/Network/NetworkCreate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ extension Application {
7373
labels: parsedLabels,
7474
pluginInfo: NetworkPluginInfo(plugin: self.plugin, variant: self.pluginVariant)
7575
)
76-
let state = try await ClientNetwork.create(configuration: config)
76+
let networkClient = NetworkClient()
77+
let state = try await networkClient.create(configuration: config)
7778
print(state.id)
7879
}
7980
}

Sources/ContainerCommands/Network/NetworkDelete.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@ extension Application {
5151
}
5252

5353
public mutating func run() async throws {
54+
let networkClient = NetworkClient()
5455
let uniqueNetworkNames = Set<String>(networkNames)
5556
let networks: [NetworkState]
5657

5758
if all {
58-
networks = try await ClientNetwork.list()
59+
networks = try await networkClient.list()
5960
.filter { !$0.isBuiltin }
6061
} else {
61-
networks = try await ClientNetwork.list()
62+
networks = try await networkClient.list()
6263
.filter { c in
6364
guard uniqueNetworkNames.contains(c.id) else {
6465
return false
@@ -96,7 +97,7 @@ extension Application {
9697
do {
9798
// Delete atomically disables the IP allocator, then deletes
9899
// the allocator. The disable fails if any IPs are still in use.
99-
try await ClientNetwork.delete(id: network.id)
100+
try await networkClient.delete(id: network.id)
100101
print(network.id)
101102
return nil
102103
} catch {

Sources/ContainerCommands/Network/NetworkInspect.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ extension Application {
3434
public init() {}
3535

3636
public func run() async throws {
37-
let items = try await ClientNetwork.list().filter {
37+
let networkClient = NetworkClient()
38+
let items = try await networkClient.list().filter {
3839
networks.contains($0.id)
3940
}.map {
4041
PrintableNetwork($0)

Sources/ContainerCommands/Network/NetworkList.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ extension Application {
4040
public init() {}
4141

4242
public func run() async throws {
43-
let networks = try await ClientNetwork.list()
43+
let networkClient = NetworkClient()
44+
let networks = try await networkClient.list()
4445
let items = networks.map { PrintableNetwork($0) }
4546
try Output.render(json: items, display: items, format: format, quiet: quiet)
4647
}

Sources/ContainerCommands/Network/NetworkPrune.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ extension Application.NetworkCommand {
3030
public var logOptions: Flags.Logging
3131

3232
public func run() async throws {
33+
let networkClient = NetworkClient()
3334
let client = ContainerClient()
3435
let allContainers = try await client.list()
35-
let allNetworks = try await ClientNetwork.list()
36+
let allNetworks = try await networkClient.list()
3637

3738
var networksInUse = Set<String>()
3839
for container in allContainers {
@@ -49,7 +50,7 @@ extension Application.NetworkCommand {
4950

5051
for network in networksToPrune {
5152
do {
52-
try await ClientNetwork.delete(id: network.id)
53+
try await networkClient.delete(id: network.id)
5354
prunedNetworks.append(network.id)
5455
} catch {
5556
// Note: This failure may occur due to a race condition between the network/

Sources/Services/ContainerAPIService/Client/ClientNetwork.swift

Lines changed: 0 additions & 97 deletions
This file was deleted.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerResource
18+
import ContainerXPC
19+
import ContainerizationError
20+
import ContainerizationExtras
21+
import ContainerizationOS
22+
import Foundation
23+
24+
/// A client for managing virtual networks through the container API server.
25+
///
26+
/// `NetworkClient` communicates with `container-apiserver` over XPC to create,
27+
/// list, inspect, and delete networks. Each instance holds a dedicated XPC
28+
/// connection; create one client and reuse it across related operations.
29+
///
30+
/// ```swift
31+
/// let client = NetworkClient()
32+
/// let state = try await client.create(configuration: config)
33+
/// let networks = try await client.list()
34+
/// try await client.delete(id: state.id)
35+
/// ```
36+
public struct NetworkClient: Sendable {
37+
/// The Mach service name used to locate the container API server.
38+
///
39+
/// Pass a different value to ``init(serviceIdentifier:)`` to connect to an
40+
/// alternative service endpoint, for example during testing.
41+
public static let defaultServiceIdentifier = "com.apple.container.apiserver"
42+
43+
/// The name of the default network created automatically on first use.
44+
public static let defaultNetworkName = "default"
45+
46+
/// The reserved name that indicates a container should have no network attachment.
47+
public static let noNetworkName = "none"
48+
49+
private let xpcClient: XPCClient
50+
51+
/// Creates a new network client connected to the given service endpoint.
52+
///
53+
/// - Parameter serviceIdentifier: The Mach service name of the API server.
54+
/// Defaults to ``defaultServiceIdentifier``.
55+
public init(serviceIdentifier: String = Self.defaultServiceIdentifier) {
56+
self.xpcClient = XPCClient(service: serviceIdentifier)
57+
}
58+
59+
@discardableResult
60+
private func xpcSend(
61+
message: XPCMessage,
62+
timeout: Duration? = XPCClient.xpcRegistrationTimeout
63+
) async throws -> XPCMessage {
64+
try await xpcClient.send(message, responseTimeout: timeout)
65+
}
66+
67+
/// Creates a new network with the given configuration.
68+
///
69+
/// The API server launches a network plugin instance for the new network and
70+
/// returns a ``NetworkState`` reflecting the network once it is running.
71+
///
72+
/// - Parameter configuration: The configuration describing the network to create.
73+
/// - Returns: The running state of the newly created network.
74+
/// - Throws: ``ContainerizationError`` if the server does not return a valid
75+
/// network state, or if the underlying XPC call fails.
76+
public func create(configuration: NetworkConfiguration) async throws -> NetworkState {
77+
let request = XPCMessage(route: .networkCreate)
78+
request.set(key: .networkId, value: configuration.id)
79+
80+
let data = try JSONEncoder().encode(configuration)
81+
request.set(key: .networkConfig, value: data)
82+
83+
let response = try await xpcSend(message: request)
84+
let responseData = response.dataNoCopy(key: .networkState)
85+
guard let responseData else {
86+
throw ContainerizationError(.invalidArgument, message: "network configuration not received")
87+
}
88+
let state = try JSONDecoder().decode(NetworkState.self, from: responseData)
89+
return state
90+
}
91+
92+
/// Returns the current state of all networks known to the API server.
93+
///
94+
/// - Returns: An array of ``NetworkState`` values, or an empty array if no
95+
/// networks exist or the server returns no data.
96+
/// - Throws: ``ContainerizationError`` if the underlying XPC call fails.
97+
public func list() async throws -> [NetworkState] {
98+
let request = XPCMessage(route: .networkList)
99+
100+
let response = try await xpcSend(message: request, timeout: .seconds(1))
101+
let responseData = response.dataNoCopy(key: .networkStates)
102+
guard let responseData else {
103+
return []
104+
}
105+
let states = try JSONDecoder().decode([NetworkState].self, from: responseData)
106+
return states
107+
}
108+
109+
/// Returns the network with the given identifier.
110+
///
111+
/// - Parameter id: The identifier of the network to look up.
112+
/// - Returns: The ``NetworkState`` for the matching network.
113+
/// - Throws: ``ContainerizationError/notFound`` if no network with the given
114+
/// identifier exists, or a communication error if the XPC call fails.
115+
public func get(id: String) async throws -> NetworkState {
116+
let networks = try await list()
117+
guard let network = networks.first(where: { $0.id == id }) else {
118+
throw ContainerizationError(.notFound, message: "network \(id) not found")
119+
}
120+
return network
121+
}
122+
123+
/// Deletes the network with the given identifier.
124+
///
125+
/// Deletion succeeds only when no containers are currently attached to the
126+
/// network. The default network cannot be deleted.
127+
///
128+
/// - Parameter id: The identifier of the network to delete.
129+
/// - Throws: ``ContainerizationError`` if the network has active attachments,
130+
/// if the network is the built-in default, or if the XPC call fails.
131+
public func delete(id: String) async throws {
132+
let request = XPCMessage(route: .networkDelete)
133+
request.set(key: .networkId, value: id)
134+
try await xpcSend(message: request)
135+
}
136+
137+
/// The built-in network, if one exists.
138+
///
139+
/// The built-in network is created automatically on first use and cannot be
140+
/// deleted. Returns `nil` if the API server cannot find a network with the
141+
/// built-in resource labels.
142+
///
143+
/// - Throws: ``ContainerizationError`` if the underlying XPC call fails.
144+
public var builtin: NetworkState? {
145+
get async throws {
146+
try await list().first { $0.isBuiltin }
147+
}
148+
}
149+
}

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,20 +195,21 @@ public struct Utility {
195195

196196
// Parse network specifications with properties
197197
let parsedNetworks = try management.networks.map { try Parser.network($0) }
198-
if management.networks.contains(ClientNetwork.noNetworkName) {
198+
if management.networks.contains(NetworkClient.noNetworkName) {
199199
guard management.networks.count == 1 else {
200-
throw ContainerizationError(.unsupported, message: "no other networks may be created along with network \(ClientNetwork.noNetworkName)")
200+
throw ContainerizationError(.unsupported, message: "no other networks may be created along with network \(NetworkClient.noNetworkName)")
201201
}
202202
config.networks = []
203203
} else {
204-
let builtinNetworkId = try await ClientNetwork.builtin?.id
204+
let networkClient = NetworkClient()
205+
let builtinNetworkId = try await networkClient.builtin?.id
205206
config.networks = try getAttachmentConfigurations(
206207
containerId: config.id,
207208
builtinNetworkId: builtinNetworkId,
208209
networks: parsedNetworks
209210
)
210211
for attachmentConfiguration in config.networks {
211-
let network: NetworkState = try await ClientNetwork.get(id: attachmentConfiguration.network)
212+
let network: NetworkState = try await networkClient.get(id: attachmentConfiguration.network)
212213
guard case .running(_, _) = network else {
213214
throw ContainerizationError(.invalidState, message: "network \(attachmentConfiguration.network) is not running")
214215
}

0 commit comments

Comments
 (0)