Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
ajemory marked this conversation as resolved.
throw ContainerizationError(.invalidState, message: "default network is not running")
}
config.networks = [
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContainerCommands/Network/NetworkCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
4 changes: 2 additions & 2 deletions Sources/ContainerCommands/Network/NetworkDelete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension Application {
public mutating func run() async throws {
let networkClient = NetworkClient()
let uniqueNetworkNames = Set<String>(networkNames)
let networks: [NetworkState]
let networks: [NetworkResource]

if all {
networks = try await networkClient.list()
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 1 addition & 6 deletions Sources/ContainerCommands/Network/NetworkInspect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import ArgumentParser
import ContainerAPIClient
import Foundation
import SwiftProtobuf

extension Application {
public struct NetworkInspect: AsyncLoggableCommand {
Expand All @@ -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))
}
}
Expand Down
43 changes: 1 addition & 42 deletions Sources/ContainerCommands/Network/NetworkList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@

import ArgumentParser
import ContainerAPIClient
import ContainerResource
import ContainerizationExtras
import Foundation
import SwiftProtobuf

extension Application {
public struct NetworkList: AsyncLoggableCommand {
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 1 addition & 9 deletions Sources/ContainerResource/Network/NetworkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
116 changes: 116 additions & 0 deletions Sources/ContainerResource/Network/NetworkResource.swift
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
jglogan marked this conversation as resolved.
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
Comment thread
ajemory marked this conversation as resolved.
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)
}
}
4 changes: 2 additions & 2 deletions Sources/ContainerResource/Network/NetworkState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions Sources/ContainerResource/Network/NetworkStatus.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Loading