Cleans up network and volume subcommands. (#661)

- Part of #515.
- Order options alphabetically.
- Use consistent delete logic for network and volume delete.

## Motivation and Context
See #515
This commit is contained in:
J Logan
2025-09-22 12:17:24 -07:00
committed by GitHub
parent f86befafc7
commit 7d905bde64
10 changed files with 113 additions and 36 deletions
@@ -18,7 +18,6 @@ import ArgumentParser
extension Application {
public struct NetworkCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "network",
abstract: "Manage container networks",
@@ -30,5 +29,7 @@ extension Application {
],
aliases: ["n"]
)
public init() {}
}
}
@@ -23,20 +23,21 @@ import TerminalProgress
extension Application {
public struct NetworkCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a new network")
@Option(name: .customLong("label"), help: "Set metadata for a network")
var labels: [String] = []
@OptionGroup
var global: Flags.Global
@Option(name: .customLong("label"), help: "Set metadata on a network")
var labels: [String] = []
@Argument(help: "Network name")
var name: String
public init() {}
public func run() async throws {
let parsedLabels = Utility.parseKeyValuePairs(labels)
let config = try NetworkConfiguration(id: self.name, mode: .nat, labels: parsedLabels)
@@ -22,21 +22,22 @@ import Foundation
extension Application {
public struct NetworkDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete one or more networks",
aliases: ["rm"])
@Flag(name: .shortAndLong, help: "Delete all networks")
var all = false
@OptionGroup
var global: Flags.Global
@Flag(name: .shortAndLong, help: "Remove all networks")
var all = false
@Argument(help: "Network names")
var networkNames: [String] = []
public init() {}
public func validate() throws {
if networkNames.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no networks specified and --all not supplied")
@@ -22,16 +22,17 @@ import SwiftProtobuf
extension Application {
public struct NetworkInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display information about one or more networks")
@Argument(help: "Networks to inspect")
var networks: [String]
@OptionGroup
var global: Flags.Global
@Argument(help: "Networks to inspect")
var networks: [String]
public init() {}
public func run() async throws {
let objects: [any Codable] = try await ClientNetwork.list().filter {
@@ -23,21 +23,22 @@ import SwiftProtobuf
extension Application {
public struct NetworkList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List networks",
aliases: ["ls"])
@Flag(name: .shortAndLong, help: "Only output the network name")
var quiet = false
@Option(name: .long, help: "Format of the output")
var format: ListFormat = .table
@Flag(name: .shortAndLong, help: "Only output the network name")
var quiet = false
@OptionGroup
var global: Flags.Global
public init() {}
public func run() async throws {
let networks = try await ClientNetwork.list()
try printNetworks(networks: networks, format: format)
@@ -18,7 +18,6 @@ import ArgumentParser
extension Application {
public struct VolumeCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "volume",
abstract: "Manage container volumes",
@@ -30,5 +29,7 @@ extension Application {
],
aliases: ["v"]
)
public init() {}
}
}
@@ -20,24 +20,28 @@ import Foundation
extension Application.VolumeCommand {
public struct VolumeCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a volume"
)
@Option(name: .customShort("s"), help: "Size of the volume (default: 512GB). Examples: 1G, 512MB, 2T")
var size: String?
@Option(name: .customLong("label"), help: "Set metadata for a volume")
var labels: [String] = []
@Option(name: .customLong("opt"), help: "Set driver specific options")
var driverOpts: [String] = []
@Option(name: .customLong("label"), help: "Set metadata on a volume")
var labels: [String] = []
@Option(name: .short, help: "Size of the volume in bytes, with optional K, M, G, T, or P suffix")
var size: String?
@OptionGroup
var global: Flags.Global
@Argument(help: "Volume name")
var name: String
public init() {}
public func run() async throws {
var parsedDriverOpts = Utility.parseKeyValuePairs(driverOpts)
let parsedLabels = Utility.parseKeyValuePairs(labels)
@@ -16,24 +16,83 @@
import ArgumentParser
import ContainerClient
import ContainerizationError
import Foundation
extension Application.VolumeCommand {
public struct VolumeDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Remove one or more volumes",
abstract: "Delete one or more volumes",
aliases: ["rm"]
)
@Argument(help: "Volume name(s)")
var names: [String]
@Flag(name: .shortAndLong, help: "Delete all volumes")
var all = false
@OptionGroup
var global: Flags.Global
@Argument(help: "Volume names")
var names: [String] = []
public init() {}
public func run() async throws {
for name in names {
try await ClientVolume.delete(name: name)
print(name)
let uniqueVolumeNames = Set<String>(names)
let volumes: [Volume]
if all {
volumes = try await ClientVolume.list()
} else {
volumes = try await ClientVolume.list()
.filter { v in
uniqueVolumeNames.contains(v.id)
}
// If one of the volumes requested isn't present lets throw. We don't need to do
// this for --all as --all should be perfectly usable with no volumes to remove,
// otherwise it'd be quite clunky.
if volumes.count != uniqueVolumeNames.count {
let missing = uniqueVolumeNames.filter { id in
!volumes.contains { v in
v.id == id
}
}
throw ContainerizationError(
.notFound,
message: "failed to delete one or more volumes: \(missing)"
)
}
}
var failed = [String]()
try await withThrowingTaskGroup(of: Volume?.self) { group in
for volume in volumes {
group.addTask {
do {
// delete atomically disables the IP allocator, then deletes
// the allocator disable fails if any IPs are still in use
try await ClientVolume.delete(name: volume.id)
print(volume.id)
return nil
} catch {
log.error("failed to delete volume \(volume.id): \(error)")
return volume
}
}
}
for try await volume in group {
guard let volume else {
continue
}
failed.append(volume.id)
}
}
if failed.count > 0 {
throw ContainerizationError(.internalError, message: "delete failed for one or more volumes: \(failed)")
}
}
}
@@ -20,15 +20,19 @@ import Foundation
extension Application.VolumeCommand {
public struct VolumeInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display detailed information on one or more volumes"
abstract: "Display information about one or more volumes"
)
@Argument(help: "Volume name(s)")
@OptionGroup
var global: Flags.Global
@Argument(help: "Volume names")
var names: [String]
public init() {}
public func run() async throws {
var volumes: [Volume] = []
@@ -21,19 +21,23 @@ import Foundation
extension Application.VolumeCommand {
public struct VolumeList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List volumes",
aliases: ["ls"]
)
@Flag(name: .shortAndLong, help: "Only display volume names")
var quiet: Bool = false
@Option(name: .long, help: "Format of the output")
var format: Application.ListFormat = .table
@Flag(name: .shortAndLong, help: "Only output the volume name")
var quiet: Bool = false
@OptionGroup
var global: Flags.Global
public init() {}
public func run() async throws {
let volumes = try await ClientVolume.list()
try printVolumes(volumes: volumes, format: format)