From b1f3c0d35bc187f42fd78fe6484e69cc2c46f88c Mon Sep 17 00:00:00 2001 From: J Logan Date: Thu, 30 Apr 2026 11:06:50 -0700 Subject: [PATCH] Use NetworkResource for network management in API server. (#1421) - Part of #1404. - Evolves API to use NetworkResource that conforms to ManagedResource. - BREAKING CHANGE - compile time impact due to changes to Swift client API signatures. No change to persistent data or API server XPC protocols. --- .../Builder/BuilderStart.swift | 2 +- .../Network/NetworkCreate.swift | 4 +- .../Network/NetworkDelete.swift | 4 +- .../Network/NetworkInspect.swift | 7 +- .../Network/NetworkList.swift | 43 +------ .../NetworkResource+ListDisplayable.swift | 31 +++++ .../Network/NetworkConfiguration.swift | 10 +- .../Network/NetworkResource.swift | 116 ++++++++++++++++++ .../Network/NetworkState.swift | 4 +- .../Network/NetworkStatus.swift | 69 +++++++++++ .../Client/NetworkClient.swift | 53 +++++--- .../ContainerAPIService/Client/Utility.swift | 4 +- .../ContainerAPIService/Client/XPC+.swift | 5 + .../Server/Networks/NetworksHarness.swift | 24 +++- .../Server/AllocationOnlyVmnetNetwork.swift | 2 +- .../Server/ReservedVmnetNetwork.swift | 2 +- .../Server/SandboxService.swift | 4 +- Tests/CLITests/Utilities/CLITest.swift | 3 +- .../ListFormattingTests.swift | 8 +- 19 files changed, 293 insertions(+), 102 deletions(-) create mode 100644 Sources/ContainerCommands/Network/NetworkResource+ListDisplayable.swift create mode 100644 Sources/ContainerResource/Network/NetworkResource.swift create mode 100644 Sources/ContainerResource/Network/NetworkStatus.swift diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index fbbc18e1..c0b3d576 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -272,7 +272,7 @@ extension Application { guard let defaultNetwork = try await networkClient.builtin else { throw ContainerizationError(.invalidState, message: "default network is not present") } - guard case .running(_, _) = defaultNetwork else { + guard defaultNetwork.status.phase == "running" else { throw ContainerizationError(.invalidState, message: "default network is not running") } config.networks = [ diff --git a/Sources/ContainerCommands/Network/NetworkCreate.swift b/Sources/ContainerCommands/Network/NetworkCreate.swift index 2c41fd78..7b770240 100644 --- a/Sources/ContainerCommands/Network/NetworkCreate.swift +++ b/Sources/ContainerCommands/Network/NetworkCreate.swift @@ -74,8 +74,8 @@ extension Application { pluginInfo: NetworkPluginInfo(plugin: self.plugin, variant: self.pluginVariant) ) let networkClient = NetworkClient() - let state = try await networkClient.create(configuration: config) - print(state.id) + let network = try await networkClient.create(configuration: config) + print(network.id) } } } diff --git a/Sources/ContainerCommands/Network/NetworkDelete.swift b/Sources/ContainerCommands/Network/NetworkDelete.swift index ea6ebbbc..91225a02 100644 --- a/Sources/ContainerCommands/Network/NetworkDelete.swift +++ b/Sources/ContainerCommands/Network/NetworkDelete.swift @@ -53,7 +53,7 @@ extension Application { public mutating func run() async throws { let networkClient = NetworkClient() let uniqueNetworkNames = Set(networkNames) - let networks: [NetworkState] + let networks: [NetworkResource] if all { networks = try await networkClient.list() @@ -91,7 +91,7 @@ extension Application { var failed = [String]() let _log = log - try await withThrowingTaskGroup(of: NetworkState?.self) { group in + try await withThrowingTaskGroup(of: NetworkResource?.self) { group in for network in networks { group.addTask { do { diff --git a/Sources/ContainerCommands/Network/NetworkInspect.swift b/Sources/ContainerCommands/Network/NetworkInspect.swift index 9bc954c5..4d8239ca 100644 --- a/Sources/ContainerCommands/Network/NetworkInspect.swift +++ b/Sources/ContainerCommands/Network/NetworkInspect.swift @@ -17,7 +17,6 @@ import ArgumentParser import ContainerAPIClient import Foundation -import SwiftProtobuf extension Application { public struct NetworkInspect: AsyncLoggableCommand { @@ -35,11 +34,7 @@ extension Application { public func run() async throws { let networkClient = NetworkClient() - let items = try await networkClient.list().filter { - networks.contains($0.id) - }.map { - PrintableNetwork($0) - } + let items = try await networkClient.list().filter { networks.contains($0.id) } try Output.emit(Output.renderJSON(items)) } } diff --git a/Sources/ContainerCommands/Network/NetworkList.swift b/Sources/ContainerCommands/Network/NetworkList.swift index 4382befa..c8e6f8cf 100644 --- a/Sources/ContainerCommands/Network/NetworkList.swift +++ b/Sources/ContainerCommands/Network/NetworkList.swift @@ -16,10 +16,7 @@ import ArgumentParser import ContainerAPIClient -import ContainerResource -import ContainerizationExtras import Foundation -import SwiftProtobuf extension Application { public struct NetworkList: AsyncLoggableCommand { @@ -42,45 +39,7 @@ extension Application { public func run() async throws { 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) - } - } -} - -extension PrintableNetwork: ListDisplayable { - public static var tableHeader: [String] { - ["NETWORK", "STATE", "SUBNET"] - } - - public var tableRow: [String] { - if let status { - return [self.id, self.state, status.ipv4Subnet.description] - } - return [self.id, self.state, "none"] - } - - public var quietValue: String { - self.id - } -} - -public struct PrintableNetwork: Codable, Sendable { - let id: String - let state: String - let config: NetworkConfiguration - let status: NetworkStatus? - - public init(_ network: NetworkState) { - self.id = network.id - self.state = network.state - switch network { - case .created(let config): - self.config = config - self.status = nil - case .running(let config, let status): - self.config = config - self.status = status + try Output.render(json: networks, display: networks, format: format, quiet: quiet) } } } diff --git a/Sources/ContainerCommands/Network/NetworkResource+ListDisplayable.swift b/Sources/ContainerCommands/Network/NetworkResource+ListDisplayable.swift new file mode 100644 index 00000000..3f3395d8 --- /dev/null +++ b/Sources/ContainerCommands/Network/NetworkResource+ListDisplayable.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// 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 + +extension NetworkResource: ListDisplayable { + public static var tableHeader: [String] { + ["NETWORK", "STATE", "SUBNET"] + } + + public var tableRow: [String] { + [id, status.phase, status.ipv4Subnet?.description ?? "none"] + } + + public var quietValue: String { + id + } +} diff --git a/Sources/ContainerResource/Network/NetworkConfiguration.swift b/Sources/ContainerResource/Network/NetworkConfiguration.swift index 348e3ee0..fbd0c889 100644 --- a/Sources/ContainerResource/Network/NetworkConfiguration.swift +++ b/Sources/ContainerResource/Network/NetworkConfiguration.swift @@ -119,16 +119,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable { } private func validate() throws { - guard id.isValidNetworkID() else { + guard NetworkResource.nameValid(id) else { throw ContainerizationError(.invalidArgument, message: "invalid network ID: \(id)") } } } - -extension String { - /// Ensure that the network ID has the correct syntax. - fileprivate func isValidNetworkID() -> Bool { - let pattern = #"^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$"# - return self.range(of: pattern, options: .regularExpression) != nil - } -} diff --git a/Sources/ContainerResource/Network/NetworkResource.swift b/Sources/ContainerResource/Network/NetworkResource.swift new file mode 100644 index 00000000..0463605a --- /dev/null +++ b/Sources/ContainerResource/Network/NetworkResource.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationExtras +import Foundation + +/// A network resource, representing a configured virtual network and its runtime status. +/// +/// `NetworkResource` conforms to `ManagedResource` and separates the network's +/// intrinsic configuration from its ephemeral runtime status — following the same +/// config/status split used by Kubernetes and Docker. `config` is persisted; +/// `status` reflects what the network plugin reports at runtime. +/// +/// The JSON encoding uses a single `status` object containing a `phase` field +/// alongside any runtime-allocated address properties, replacing the prior flat +/// `state`/`status` pair in the CLI output. +public struct NetworkResource: ManagedResource { + /// The network's configuration — its persistent, intrinsic properties. + public let config: NetworkConfiguration + + /// The network's current status, including lifecycle phase and any + /// runtime-allocated address properties. + public let status: NetworkStatus + + // MARK: ManagedResource + + /// The unique identifier for this network. Identical to ``config/id``. + public var id: String { config.id } + + /// The user-assigned name for this network. For networks, name and ID are the same. + public var name: String { config.id } + + /// The time at which this network was created. + public var creationDate: Date { config.creationDate } + + /// Key-value labels for this network. + public var labels: ResourceLabels { config.labels } + + /// Returns `true` for a system-managed network that cannot be deleted by the user. + public var isBuiltin: Bool { labels.isBuiltin } + + /// Returns `true` if `name` is a syntactically valid network identifier. + /// + /// Valid network names are lowercase alphanumeric strings of up to 63 + /// characters, allowing dots, hyphens, and underscores in interior positions. + public static func nameValid(_ name: String) -> Bool { + let pattern = #"^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$"# + return name.range(of: pattern, options: .regularExpression) != nil + } + + // MARK: Initialization + + /// Creates a network resource. + /// + /// - Parameters: + /// - config: The network's intrinsic configuration. + /// - networkStatus: The plugin-reported runtime status, or `nil` if the + /// network is not yet running. + public init(config: NetworkConfiguration, networkStatus: NetworkPluginStatus? = nil) { + self.config = config + self.status = networkStatus.map { NetworkStatus(running: $0) } ?? .created + } +} + +// MARK: - Conversion from NetworkState + +extension NetworkResource { + /// Creates a network resource from a ``NetworkState``. + /// + /// Used when translating from the internal plugin-protocol type to the + /// public API surface type. + public init(_ networkState: NetworkState) { + switch networkState { + case .created(let config): + self.init(config: config) + case .running(let config, let status): + self.init(config: config, networkStatus: status) + } + } +} + +// MARK: - Codable + +extension NetworkResource { + enum CodingKeys: String, CodingKey { + case id + case config + case status + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(config, forKey: .config) + try container.encode(status, forKey: .status) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.config = try container.decode(NetworkConfiguration.self, forKey: .config) + self.status = try container.decode(NetworkStatus.self, forKey: .status) + } +} diff --git a/Sources/ContainerResource/Network/NetworkState.swift b/Sources/ContainerResource/Network/NetworkState.swift index 2c219ea0..6965ea7b 100644 --- a/Sources/ContainerResource/Network/NetworkState.swift +++ b/Sources/ContainerResource/Network/NetworkState.swift @@ -17,7 +17,7 @@ import ContainerizationExtras import Foundation -public struct NetworkStatus: Codable, Sendable { +public struct NetworkPluginStatus: Codable, Sendable { /// The address allocated for the network if no subnet was specified at /// creation time; otherwise, the subnet from the configuration. public let ipv4Subnet: CIDRv4 @@ -83,7 +83,7 @@ public enum NetworkState: Codable, Sendable { // The network has been configured. case created(NetworkConfiguration) // The network is running. - case running(NetworkConfiguration, NetworkStatus) + case running(NetworkConfiguration, NetworkPluginStatus) public var state: String { switch self { diff --git a/Sources/ContainerResource/Network/NetworkStatus.swift b/Sources/ContainerResource/Network/NetworkStatus.swift new file mode 100644 index 00000000..99baa6b3 --- /dev/null +++ b/Sources/ContainerResource/Network/NetworkStatus.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationExtras +import Foundation + +/// The runtime status of a network resource. +/// +/// `phase` names the current lifecycle stage; the address fields are present +/// only when `phase` is `"running"` and are `nil` otherwise. Clients should +/// treat unrecognised `phase` values as unknown forward-compatible stages rather +/// than treating them as errors. +public struct NetworkStatus: Codable, Sendable { + /// The current lifecycle phase of the network. + /// + /// Defined values: `"created"` (configured, plugin not yet active) and + /// `"running"` (plugin active, subnet and gateway assigned). + public let phase: String + + /// The allocated IPv4 subnet. Present only when `phase` is `"running"`. + public let ipv4Subnet: CIDRv4? + + /// The IPv4 gateway address. Present only when `phase` is `"running"`. + public let ipv4Gateway: IPv4Address? + + /// The allocated IPv6 subnet. Present only when `phase` is `"running"` and + /// the network has IPv6 enabled. + public let ipv6Subnet: CIDRv6? + + public init( + phase: String, + ipv4Subnet: CIDRv4? = nil, + ipv4Gateway: IPv4Address? = nil, + ipv6Subnet: CIDRv6? = nil + ) { + self.phase = phase + self.ipv4Subnet = ipv4Subnet + self.ipv4Gateway = ipv4Gateway + self.ipv6Subnet = ipv6Subnet + } +} + +extension NetworkStatus { + /// The status value for a network that is configured but not yet running. + public static let created = NetworkStatus(phase: "created") + + /// Creates a running-phase status from a ``NetworkPluginStatus``. + init(running networkStatus: NetworkPluginStatus) { + self.init( + phase: "running", + ipv4Subnet: networkStatus.ipv4Subnet, + ipv4Gateway: networkStatus.ipv4Gateway, + ipv6Subnet: networkStatus.ipv6Subnet + ) + } +} diff --git a/Sources/Services/ContainerAPIService/Client/NetworkClient.swift b/Sources/Services/ContainerAPIService/Client/NetworkClient.swift index fe2f4b0a..24683036 100644 --- a/Sources/Services/ContainerAPIService/Client/NetworkClient.swift +++ b/Sources/Services/ContainerAPIService/Client/NetworkClient.swift @@ -29,9 +29,9 @@ import Foundation /// /// ```swift /// let client = NetworkClient() -/// let state = try await client.create(configuration: config) +/// let network = try await client.create(configuration: config) /// let networks = try await client.list() -/// try await client.delete(id: state.id) +/// try await client.delete(id: network.id) /// ``` public struct NetworkClient: Sendable { /// The Mach service name used to locate the container API server. @@ -67,13 +67,13 @@ public struct NetworkClient: Sendable { /// 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. + /// returns a ``NetworkResource`` 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 { + /// network resource, or if the underlying XPC call fails. + public func create(configuration: NetworkConfiguration) async throws -> NetworkResource { let request = XPCMessage(route: .networkCreate) request.set(key: .networkId, value: configuration.id) @@ -81,38 +81,51 @@ public struct NetworkClient: Sendable { 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") + + // Prefer current encoding (≥ 0.12.0 server). + if let resourceData = response.dataNoCopy(key: .networkResource) { + return try JSONDecoder().decode(NetworkResource.self, from: resourceData) } - let state = try JSONDecoder().decode(NetworkState.self, from: responseData) - return state + + // Fall back to pre-0.12.0 server: decode NetworkState and convert. + if let stateData = response.dataNoCopy(key: .networkState) { + let state = try JSONDecoder().decode(NetworkState.self, from: stateData) + return NetworkResource(state) + } + + throw ContainerizationError(.invalidArgument, message: "network configuration not received") } /// Returns the current state of all networks known to the API server. /// - /// - Returns: An array of ``NetworkState`` values, or an empty array if no + /// - Returns: An array of ``NetworkResource`` 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] { + public func list() async throws -> [NetworkResource] { 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 [] + + // Prefer current encoding (≥ 0.12.0 server). + if let resourceData = response.dataNoCopy(key: .networkResources) { + return try JSONDecoder().decode([NetworkResource].self, from: resourceData) } - let states = try JSONDecoder().decode([NetworkState].self, from: responseData) - return states + + // Fall back to pre-0.12.0 server: decode NetworkState and convert. + if let stateData = response.dataNoCopy(key: .networkStates) { + return try JSONDecoder().decode([NetworkState].self, from: stateData).map(NetworkResource.init) + } + + return [] } /// Returns the network with the given identifier. /// /// - Parameter id: The identifier of the network to look up. - /// - Returns: The ``NetworkState`` for the matching network. + /// - Returns: The ``NetworkResource`` 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 { + public func get(id: String) async throws -> NetworkResource { let networks = try await list() guard let network = networks.first(where: { $0.id == id }) else { throw ContainerizationError(.notFound, message: "network \(id) not found") @@ -141,7 +154,7 @@ public struct NetworkClient: Sendable { /// built-in resource labels. /// /// - Throws: ``ContainerizationError`` if the underlying XPC call fails. - public var builtin: NetworkState? { + public var builtin: NetworkResource? { get async throws { try await list().first { $0.isBuiltin } } diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index 023ed2c7..e404aa2b 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -209,8 +209,8 @@ public struct Utility { networks: parsedNetworks ) for attachmentConfiguration in config.networks { - let network: NetworkState = try await networkClient.get(id: attachmentConfiguration.network) - guard case .running(_, _) = network else { + let network = try await networkClient.get(id: attachmentConfiguration.network) + guard network.status.phase == "running" else { throw ContainerizationError(.invalidState, message: "network \(attachmentConfiguration.network) is not running") } } diff --git a/Sources/Services/ContainerAPIService/Client/XPC+.swift b/Sources/Services/ContainerAPIService/Client/XPC+.swift index 9ddbaafe..033cfb4f 100644 --- a/Sources/Services/ContainerAPIService/Client/XPC+.swift +++ b/Sources/Services/ContainerAPIService/Client/XPC+.swift @@ -103,6 +103,11 @@ public enum XPCKeys: String { case networkConfig case networkState case networkStates + // Added in 0.12.0: NetworkResource encoding (status.phase shape). + // DEPRECATED 0.12.0: networkState/networkStates retained for down-revision + // client compatibility; remove at next major version boundary. + case networkResource + case networkResources /// Kernel case kernel diff --git a/Sources/Services/ContainerAPIService/Server/Networks/NetworksHarness.swift b/Sources/Services/ContainerAPIService/Server/Networks/NetworksHarness.swift index 00b273dd..6c5efa55 100644 --- a/Sources/Services/ContainerAPIService/Server/Networks/NetworksHarness.swift +++ b/Sources/Services/ContainerAPIService/Server/Networks/NetworksHarness.swift @@ -32,11 +32,18 @@ public struct NetworksHarness: Sendable { @Sendable public func list(_ message: XPCMessage) async throws -> XPCMessage { - let containers = try await service.list() - let data = try JSONEncoder().encode(containers) + let states = try await service.list() let reply = message.reply() - reply.set(key: .networkStates, value: data) + + // Current encoding: NetworkResource with status.phase shape (≥ 0.12.0). + let resources = states.map(NetworkResource.init) + reply.set(key: .networkResources, value: try JSONEncoder().encode(resources)) + + // DEPRECATED 0.12.0 — retained for down-revision client compatibility. + // Remove at next major version boundary. + reply.set(key: .networkStates, value: try JSONEncoder().encode(states)) + return reply } @@ -50,10 +57,15 @@ public struct NetworksHarness: Sendable { let config = try JSONDecoder().decode(NetworkConfiguration.self, from: data) let networkState = try await service.create(configuration: config) - let networkData = try JSONEncoder().encode(networkState) - let reply = message.reply() - reply.set(key: .networkState, value: networkData) + + // Current encoding: NetworkResource with status.phase shape (≥ 0.12.0). + reply.set(key: .networkResource, value: try JSONEncoder().encode(NetworkResource(networkState))) + + // DEPRECATED 0.12.0 — retained for down-revision client compatibility. + // Remove at next major version boundary. + reply.set(key: .networkState, value: try JSONEncoder().encode(networkState)) + return reply } diff --git a/Sources/Services/ContainerNetworkService/Server/AllocationOnlyVmnetNetwork.swift b/Sources/Services/ContainerNetworkService/Server/AllocationOnlyVmnetNetwork.swift index be6330d2..d63e4851 100644 --- a/Sources/Services/ContainerNetworkService/Server/AllocationOnlyVmnetNetwork.swift +++ b/Sources/Services/ContainerNetworkService/Server/AllocationOnlyVmnetNetwork.swift @@ -70,7 +70,7 @@ public actor AllocationOnlyVmnetNetwork: Network { let ipv4Subnet = configuration.ipv4Subnet ?? Self.defaultIPv4Subnet let gateway = IPv4Address(ipv4Subnet.lower.value + 1) - let status = NetworkStatus( + let status = NetworkPluginStatus( ipv4Subnet: ipv4Subnet, ipv4Gateway: gateway, ipv6Subnet: nil, diff --git a/Sources/Services/ContainerNetworkService/Server/ReservedVmnetNetwork.swift b/Sources/Services/ContainerNetworkService/Server/ReservedVmnetNetwork.swift index 2076378b..39ef9c2a 100644 --- a/Sources/Services/ContainerNetworkService/Server/ReservedVmnetNetwork.swift +++ b/Sources/Services/ContainerNetworkService/Server/ReservedVmnetNetwork.swift @@ -80,7 +80,7 @@ public final class ReservedVmnetNetwork: Network { let networkInfo = try startNetwork(configuration: configuration, log: log) - let networkStatus = NetworkStatus( + let networkStatus = NetworkPluginStatus( ipv4Subnet: networkInfo.ipv4Subnet, ipv4Gateway: networkInfo.ipv4Gateway, ipv6Subnet: networkInfo.ipv6Subnet, diff --git a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift index c91a822f..a13d4e24 100644 --- a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift @@ -917,10 +917,10 @@ public actor SandboxService { let networkClient = NetworkClient() for allocatedAttach in allocatedAttachments { let state = try await networkClient.get(id: allocatedAttach.attachment.network) - guard case .running(_, let status) = state else { + guard state.status.phase == "running", let gateway = state.status.ipv4Gateway else { continue } - return [status.ipv4Gateway.description] + return [gateway.description] } return [] diff --git a/Tests/CLITests/Utilities/CLITest.swift b/Tests/CLITests/Utilities/CLITest.swift index 7bbe63cf..28d0bef8 100644 --- a/Tests/CLITests/Utilities/CLITest.swift +++ b/Tests/CLITests/Utilities/CLITest.swift @@ -46,9 +46,8 @@ class CLITest { struct NetworkInspectOutput: Codable { let id: String - let state: String let config: NetworkConfiguration - let status: NetworkStatus? + let status: NetworkStatus } let testName: String diff --git a/Tests/ContainerCommandsTests/ListFormattingTests.swift b/Tests/ContainerCommandsTests/ListFormattingTests.swift index 7d870c01..ccb9916a 100644 --- a/Tests/ContainerCommandsTests/ListFormattingTests.swift +++ b/Tests/ContainerCommandsTests/ListFormattingTests.swift @@ -236,13 +236,13 @@ struct PrintableContainerDisplayTests { } } -// MARK: - PrintableNetwork conformance tests +// MARK: - NetworkResource ListDisplayable conformance tests -struct PrintableNetworkDisplayTests { +struct NetworkResourceDisplayTests { @Test func tableHeaderHasThreeColumns() { - #expect(PrintableNetwork.tableHeader.count == 3) - #expect(PrintableNetwork.tableHeader == ["NETWORK", "STATE", "SUBNET"]) + #expect(NetworkResource.tableHeader.count == 3) + #expect(NetworkResource.tableHeader == ["NETWORK", "STATE", "SUBNET"]) } }