Expose Command Structs for Plugins (#603)

## Type of Change
- [ ] Bug fix
- [x] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
Plugins technically exist, but to add shortcuts or to do existing things
with functions in `container` requires calling a compiled binary. This
pull request aims to remove that hurdle and instability by exposing
commands as a new `ContainerCommands ` target.

Simply import `ContainerCommands` and you can access almost
any command as if it were a native part of the binary. This makes
plugin development significantly easier.

Closes #609.
This commit is contained in:
Morris Richman
2025-09-17 15:24:26 -07:00
committed by GitHub
parent 3e52a3c305
commit dd6bdc20cf
56 changed files with 314 additions and 190 deletions
+10
View File
@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerSandboxService", targets: ["ContainerSandboxService"]),
.library(name: "ContainerNetworkService", targets: ["ContainerNetworkService"]),
.library(name: "ContainerImagesService", targets: ["ContainerImagesService", "ContainerImagesServiceClient"]),
.library(name: "ContainerCommands", targets: ["ContainerCommands"]),
.library(name: "ContainerClient", targets: ["ContainerClient"]),
.library(name: "ContainerBuild", targets: ["ContainerBuild"]),
.library(name: "ContainerLog", targets: ["ContainerLog"]),
@@ -65,6 +66,15 @@ let package = Package(
targets: [
.executableTarget(
name: "container",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"ContainerClient",
"ContainerCommands",
],
path: "Sources/ExecutableCLI"
),
.target(
name: "ContainerCommands",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
+8 -7
View File
@@ -35,12 +35,13 @@ nonisolated(unsafe) var log = {
return log
}()
@main
struct Application: AsyncParsableCommand {
public struct Application: AsyncParsableCommand {
@OptionGroup
var global: Flags.Global
static let configuration = CommandConfiguration(
public init() {}
public static let configuration = CommandConfiguration(
commandName: "container",
abstract: "A container platform for macOS",
version: ReleaseVersion.singleLine(appName: "container CLI"),
@@ -126,7 +127,7 @@ struct Application: AsyncParsableCommand {
}
}
static func createPluginLoader() async throws -> PluginLoader {
public static func createPluginLoader() async throws -> PluginLoader {
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
@@ -170,7 +171,7 @@ struct Application: AsyncParsableCommand {
)
}
static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
public static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
let signals = AsyncSignalHandler.create(notify: Application.signalSet)
return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in
let waitAdded = group.addTaskUnlessCancelled {
@@ -242,7 +243,7 @@ struct Application: AsyncParsableCommand {
}
}
func validate() throws {
public func validate() throws {
// Not really a "validation", but a cheat to run this before
// any of the commands do their business.
let debugEnvVar = ProcessInfo.processInfo.environment["CONTAINER_DEBUG"]
@@ -308,7 +309,7 @@ extension Application {
print(altered)
}
enum ListFormat: String, CaseIterable, ExpressibleByArgument {
public enum ListFormat: String, CaseIterable, ExpressibleByArgument {
case json
case table
}
+11 -10
View File
@@ -27,7 +27,8 @@ import NIO
import TerminalProgress
extension Application {
struct BuildCommand: AsyncParsableCommand {
public struct BuildCommand: AsyncParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "build"
@@ -38,7 +39,7 @@ extension Application {
}
@Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container")
public var cpus: Int64 = 2
var cpus: Int64 = 2
@Option(
name: [.customLong("memory"), .customShort("m")],
@@ -117,7 +118,7 @@ extension Application {
@Flag(name: .shortAndLong, help: "Suppress build output")
var quiet: Bool = false
func run() async throws {
public func run() async throws {
do {
let timeout: Duration = .seconds(300)
let progressConfig = try ProgressConfig(
@@ -132,16 +133,16 @@ extension Application {
progress.set(description: "Dialing builder")
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { group in
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory] group in
defer {
group.cancelAll()
}
group.addTask {
group.addTask { [vsockPort, cpus, memory] in
while true {
do {
let container = try await ClientContainer.get(id: "buildkit")
let fh = try await container.dial(self.vsockPort)
let fh = try await container.dial(vsockPort)
let threadGroup: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let b = try Builder(socket: fh, group: threadGroup)
@@ -156,8 +157,8 @@ extension Application {
progress.set(totalTasks: 3)
try await BuilderStart.start(
cpus: self.cpus,
memory: self.memory,
cpus: cpus,
memory: memory,
progressUpdate: progress.handler
)
@@ -255,7 +256,7 @@ extension Application {
}
return results
}()
group.addTask { [terminal] in
group.addTask { [terminal, buildArg, contextDir, label, noCache, target, quiet, cacheIn, cacheOut] in
let config = ContainerBuild.Builder.BuildConfig(
buildID: buildID,
contentStore: RemoteContentStoreClient(),
@@ -341,7 +342,7 @@ extension Application {
}
}
func validate() throws {
public func validate() throws {
guard FileManager.default.fileExists(atPath: file) else {
throw ValidationError("Dockerfile does not exist at path: \(file)")
}
+4 -2
View File
@@ -17,8 +17,10 @@
import ArgumentParser
extension Application {
struct BuilderCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct BuilderCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "builder",
abstract: "Manage an image builder instance",
subcommands: [
+4 -2
View File
@@ -20,7 +20,9 @@ import ContainerizationError
import Foundation
extension Application {
struct BuilderDelete: AsyncParsableCommand {
public struct BuilderDelete: AsyncParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "delete"
@@ -35,7 +37,7 @@ extension Application {
@Flag(name: .shortAndLong, help: "Force delete builder even if it is running")
var force = false
func run() async throws {
public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
if container.status != .stopped {
+6 -4
View File
@@ -27,7 +27,9 @@ import Foundation
import TerminalProgress
extension Application {
struct BuilderStart: AsyncParsableCommand {
public struct BuilderStart: AsyncParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "start"
@@ -39,16 +41,16 @@ extension Application {
}
@Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container")
public var cpus: Int64 = 2
var cpus: Int64 = 2
@Option(
name: [.customLong("memory"), .customShort("m")],
help:
"Amount of memory in bytes, kilobytes (K), megabytes (M), or gigabytes (G) for the container, with MB granularity (for example, 1024K will result in 1MB being allocated for the container)"
)
public var memory: String = "2048MB"
var memory: String = "2048MB"
func run() async throws {
public func run() async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
+4 -2
View File
@@ -20,7 +20,9 @@ import ContainerizationError
import Foundation
extension Application {
struct BuilderStatus: AsyncParsableCommand {
public struct BuilderStatus: AsyncParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "status"
@@ -34,7 +36,7 @@ extension Application {
@Flag(name: .long, help: ArgumentHelp("Display detailed status in json format"))
var json: Bool = false
func run() async throws {
public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
if json {
+4 -2
View File
@@ -20,7 +20,9 @@ import ContainerizationError
import Foundation
extension Application {
struct BuilderStop: AsyncParsableCommand {
public struct BuilderStop: AsyncParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "stop"
@@ -31,7 +33,7 @@ extension Application {
return config
}
func run() async throws {
public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
try await container.stop()
+5 -3
View File
@@ -21,8 +21,10 @@ import Foundation
import TerminalProgress
extension Application {
struct ContainerCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a new container")
@@ -47,7 +49,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func run() async throws {
public func run() async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
+6 -4
View File
@@ -20,8 +20,10 @@ import ContainerizationError
import Foundation
extension Application {
struct ContainerDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete one or more containers",
aliases: ["rm"])
@@ -38,7 +40,7 @@ extension Application {
@Argument(help: "Container IDs/names")
var containerIDs: [String] = []
func validate() throws {
public func validate() throws {
if containerIDs.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied")
}
@@ -50,7 +52,7 @@ extension Application {
}
}
mutating func run() async throws {
public mutating func run() async throws {
let set = Set<String>(containerIDs)
var containers = [ClientContainer]()
+5 -3
View File
@@ -21,8 +21,10 @@ import ContainerizationOS
import Foundation
extension Application {
struct ContainerExec: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerExec: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "exec",
abstract: "Run a new command in a running container")
@@ -38,7 +40,7 @@ extension Application {
@Argument(parsing: .captureForPassthrough, help: "New process arguments")
var arguments: [String]
func run() async throws {
public func run() async throws {
var exitCode: Int32 = 127
let container = try await ClientContainer.get(id: containerID)
try ensureRunning(container: container)
+5 -3
View File
@@ -20,8 +20,10 @@ import Foundation
import SwiftProtobuf
extension Application {
struct ContainerInspect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display information about one or more containers")
@@ -31,7 +33,7 @@ extension Application {
@Argument(help: "Containers to inspect")
var containers: [String]
func run() async throws {
public func run() async throws {
let objects: [any Codable] = try await ClientContainer.list().filter {
containers.contains($0.id)
}.map {
+6 -4
View File
@@ -21,8 +21,10 @@ import ContainerizationOS
import Darwin
extension Application {
struct ContainerKill: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerKill: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "kill",
abstract: "Kill one or more running containers")
@@ -38,7 +40,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func validate() throws {
public func validate() throws {
if containerIDs.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied")
}
@@ -47,7 +49,7 @@ extension Application {
}
}
mutating func run() async throws {
public mutating func run() async throws {
let set = Set<String>(containerIDs)
var containers = try await ClientContainer.list().filter { c in
+5 -3
View File
@@ -22,8 +22,10 @@ import Foundation
import SwiftProtobuf
extension Application {
struct ContainerList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List containers",
aliases: ["ls"])
@@ -40,7 +42,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func run() async throws {
public func run() async throws {
let containers = try await ClientContainer.list()
try printContainers(containers: containers, format: format)
}
+5 -3
View File
@@ -22,8 +22,10 @@ import Dispatch
import Foundation
extension Application {
struct ContainerLogs: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerLogs: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "logs",
abstract: "Fetch container stdio or boot logs"
)
@@ -43,7 +45,7 @@ extension Application {
@Argument(help: "Container to fetch logs for")
var container: String
func run() async throws {
public func run() async throws {
do {
let container = try await ClientContainer.get(id: container)
let fhs = try await container.logs()
+5 -3
View File
@@ -21,8 +21,10 @@ import ContainerizationOS
import TerminalProgress
extension Application {
struct ContainerStart: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerStart: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Start a container")
@@ -38,7 +40,7 @@ extension Application {
@Argument(help: "Container's ID")
var containerID: String
func run() async throws {
public func run() async throws {
var exitCode: Int32 = 127
let progressConfig = try ProgressConfig(
+6 -4
View File
@@ -21,8 +21,10 @@ import ContainerizationOS
import Foundation
extension Application {
struct ContainerStop: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerStop: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "stop",
abstract: "Stop one or more running containers")
@@ -41,7 +43,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func validate() throws {
public func validate() throws {
if containerIDs.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied")
}
@@ -51,7 +53,7 @@ extension Application {
}
}
mutating func run() async throws {
public mutating func run() async throws {
let set = Set<String>(containerIDs)
var containers = [ClientContainer]()
if self.all {
@@ -17,8 +17,10 @@
import ArgumentParser
extension Application {
struct ContainersCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainersCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "containers",
abstract: "Manage containers",
subcommands: [
+1 -1
View File
@@ -21,7 +21,7 @@ import Darwin
import Foundation
struct DefaultCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public static let configuration = CommandConfiguration(
commandName: nil,
shouldDisplay: false
)
+4 -2
View File
@@ -17,8 +17,10 @@
import ArgumentParser
extension Application {
struct ImageCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "image",
abstract: "Manage images",
subcommands: [
+4 -3
View File
@@ -21,8 +21,9 @@ import Foundation
import SwiftProtobuf
extension Application {
struct ImageInspect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display information about one or more images")
@@ -32,7 +33,7 @@ extension Application {
@Argument(help: "Images to inspect")
var images: [String]
func run() async throws {
public func run() async throws {
var printable = [any Codable]()
let result = try await ClientImage.get(names: images)
let notFound = result.error
+10 -8
View File
@@ -23,7 +23,8 @@ import Foundation
import SwiftProtobuf
extension Application {
struct ListImageOptions: ParsableArguments {
public struct ListImageOptions: ParsableArguments {
public init() {}
@Flag(name: .shortAndLong, help: "Only output the image name")
var quiet = false
@@ -38,15 +39,15 @@ extension Application {
}
struct ListImageImplementation {
static private func createHeader() -> [[String]] {
static func createHeader() -> [[String]] {
[["NAME", "TAG", "DIGEST"]]
}
static private func createVerboseHeader() -> [[String]] {
static func createVerboseHeader() -> [[String]] {
[["NAME", "TAG", "INDEX DIGEST", "OS", "ARCH", "VARIANT", "SIZE", "CREATED", "MANIFEST DIGEST"]]
}
static private func printImagesVerbose(images: [ClientImage]) async throws {
static func printImagesVerbose(images: [ClientImage]) async throws {
var rows = createVerboseHeader()
for image in images {
@@ -102,7 +103,7 @@ extension Application {
print(formatter.format())
}
static private func printImages(images: [ClientImage], format: ListFormat, options: ListImageOptions) async throws {
static func printImages(images: [ClientImage], format: ListFormat, options: ListImageOptions) async throws {
var images = images
images.sort {
$0.reference < $1.reference
@@ -160,8 +161,9 @@ extension Application {
}
}
struct ImageList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List images",
aliases: ["ls"])
@@ -169,7 +171,7 @@ extension Application {
@OptionGroup
var options: ListImageOptions
mutating func run() async throws {
public mutating func run() async throws {
try ListImageImplementation.validate(options: options)
try await ListImageImplementation.listImages(options: options)
}
+4 -3
View File
@@ -22,8 +22,9 @@ import Foundation
import TerminalProgress
extension Application {
struct ImageLoad: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageLoad: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "load",
abstract: "Load images from an OCI compatible tar archive"
)
@@ -38,7 +39,7 @@ extension Application {
})
var input: String
func run() async throws {
public func run() async throws {
guard FileManager.default.fileExists(atPath: input) else {
print("File does not exist \(input)")
Application.exit(withError: ArgumentParser.ExitCode(1))
+4 -3
View File
@@ -19,15 +19,16 @@ import ContainerClient
import Foundation
extension Application {
struct ImagePrune: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImagePrune: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "prune",
abstract: "Remove unreferenced and dangling images")
@OptionGroup
var global: Flags.Global
func run() async throws {
public func run() async throws {
let (_, size) = try await ClientImage.pruneImages()
let formatter = ByteCountFormatter()
let freed = formatter.string(fromByteCount: Int64(size))
+5 -5
View File
@@ -22,8 +22,8 @@ import ContainerizationOCI
import TerminalProgress
extension Application {
struct ImagePull: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImagePull: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "pull",
abstract: "Pull an image"
)
@@ -55,9 +55,9 @@ extension Application {
@Argument var reference: String
init() {}
public init() {}
init(platform: String? = nil, scheme: String = "auto", reference: String, disableProgress: Bool = false) {
public init(platform: String? = nil, scheme: String = "auto", reference: String, disableProgress: Bool = false) {
self.global = Flags.Global()
self.registry = Flags.Registry(scheme: scheme)
self.progressFlags = Flags.Progress(disableProgressUpdates: disableProgress)
@@ -65,7 +65,7 @@ extension Application {
self.reference = reference
}
func run() async throws {
public func run() async throws {
var p: Platform?
if let platform {
p = try Platform(from: platform)
+4 -3
View File
@@ -21,8 +21,9 @@ import ContainerizationOCI
import TerminalProgress
extension Application {
struct ImagePush: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImagePush: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "push",
abstract: "Push an image"
)
@@ -40,7 +41,7 @@ extension Application {
@Argument var reference: String
func run() async throws {
public func run() async throws {
var p: Platform?
if let platform {
p = try Platform(from: platform)
+10 -6
View File
@@ -21,7 +21,9 @@ import ContainerizationError
import Foundation
extension Application {
struct RemoveImageOptions: ParsableArguments {
public struct RemoveImageOptions: ParsableArguments {
public init() {}
@Flag(name: .shortAndLong, help: "Remove all images")
var all: Bool = false
@@ -32,7 +34,7 @@ extension Application {
var global: Flags.Global
}
struct RemoveImageImplementation {
public struct RemoveImageImplementation {
static func validate(options: RemoveImageOptions) throws {
if options.images.count == 0 && !options.all {
throw ContainerizationError(.invalidArgument, message: "no image specified and --all not supplied")
@@ -79,20 +81,22 @@ extension Application {
}
}
struct ImageRemove: AsyncParsableCommand {
public struct ImageRemove: AsyncParsableCommand {
public init() {}
@OptionGroup
var options: RemoveImageOptions
static let configuration = CommandConfiguration(
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Remove one or more images",
aliases: ["rm"])
func validate() throws {
public func validate() throws {
try RemoveImageImplementation.validate(options: options)
}
mutating func run() async throws {
public mutating func run() async throws {
try await RemoveImageImplementation.removeImage(options: options)
}
}
+4 -3
View File
@@ -23,8 +23,9 @@ import Foundation
import TerminalProgress
extension Application {
struct ImageSave: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageSave: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "save",
abstract: "Save an image as an OCI compatible tar archive"
)
@@ -57,7 +58,7 @@ extension Application {
@Argument var references: [String]
func run() async throws {
public func run() async throws {
var p: Platform?
if let platform {
p = try Platform(from: platform)
+4 -3
View File
@@ -18,8 +18,9 @@ import ArgumentParser
import ContainerClient
extension Application {
struct ImageTag: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ImageTag: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "tag",
abstract: "Tag an image")
@@ -32,7 +33,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func run() async throws {
public func run() async throws {
let existing = try await ClientImage.get(reference: source)
let targetReference = try ClientImage.normalizeReference(target)
try await existing.tag(new: targetReference)
+3 -2
View File
@@ -17,8 +17,9 @@
import ArgumentParser
extension Application {
struct NetworkCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct NetworkCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "network",
abstract: "Manage container networks",
subcommands: [
+4 -3
View File
@@ -22,8 +22,9 @@ import Foundation
import TerminalProgress
extension Application {
struct NetworkCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct NetworkCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a new network")
@@ -36,7 +37,7 @@ extension Application {
@Argument(help: "Network name")
var name: String
func run() async throws {
public func run() async throws {
let parsedLabels = Utility.parseKeyValuePairs(labels)
let config = try NetworkConfiguration(id: self.name, mode: .nat, labels: parsedLabels)
let state = try await ClientNetwork.create(configuration: config)
+5 -4
View File
@@ -21,8 +21,9 @@ import ContainerizationError
import Foundation
extension Application {
struct NetworkDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct NetworkDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete one or more networks",
aliases: ["rm"])
@@ -36,7 +37,7 @@ extension Application {
@Argument(help: "Network names")
var networkNames: [String] = []
func validate() throws {
public func validate() throws {
if networkNames.count == 0 && !all {
throw ContainerizationError(.invalidArgument, message: "no networks specified and --all not supplied")
}
@@ -48,7 +49,7 @@ extension Application {
}
}
mutating func run() async throws {
public mutating func run() async throws {
let uniqueNetworkNames = Set<String>(networkNames)
let networks: [NetworkState]
+4 -3
View File
@@ -21,8 +21,9 @@ import Foundation
import SwiftProtobuf
extension Application {
struct NetworkInspect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct NetworkInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display information about one or more networks")
@@ -32,7 +33,7 @@ extension Application {
@Argument(help: "Networks to inspect")
var networks: [String]
func run() async throws {
public func run() async throws {
let objects: [any Codable] = try await ClientNetwork.list().filter {
networks.contains($0.id)
}.map {
+10 -9
View File
@@ -22,22 +22,23 @@ import Foundation
import SwiftProtobuf
extension Application {
struct NetworkList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct NetworkList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List networks",
aliases: ["ls"])
@OptionGroup
var global: Flags.Global
@Flag(name: .shortAndLong, help: "Only output the network name")
var quiet = false
@Option(name: .long, help: "Format of the output")
var format: ListFormat = .table
func run() async throws {
@OptionGroup
var global: Flags.Global
public func run() async throws {
let networks = try await ClientNetwork.list()
try printNetworks(networks: networks, format: format)
}
@@ -46,7 +47,7 @@ extension Application {
[["NETWORK", "STATE", "SUBNET"]]
}
private func printNetworks(networks: [NetworkState], format: ListFormat) throws {
func printNetworks(networks: [NetworkState], format: ListFormat) throws {
if format == .json {
let printables = networks.map {
PrintableNetwork($0)
@@ -86,13 +87,13 @@ extension NetworkState {
}
}
struct PrintableNetwork: Codable {
public struct PrintableNetwork: Codable {
let id: String
let state: String
let config: NetworkConfiguration
let status: NetworkStatus?
init(_ network: NetworkState) {
public init(_ network: NetworkState) {
self.id = network.id
self.state = network.state
switch network {
+4 -3
View File
@@ -22,8 +22,9 @@ import ContainerizationOCI
import Foundation
extension Application {
struct Login: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct Login: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
abstract: "Login to a registry"
)
@@ -39,7 +40,7 @@ extension Application {
@OptionGroup
var registry: Flags.Registry
func run() async throws {
public func run() async throws {
var username = self.username
var password = ""
if passwordStdin {
+4 -3
View File
@@ -20,8 +20,9 @@ import Containerization
import ContainerizationOCI
extension Application {
struct Logout: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct Logout: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
abstract: "Log out from a registry")
@Argument(help: "Registry server name")
@@ -30,7 +31,7 @@ extension Application {
@OptionGroup
var global: Flags.Global
func run() async throws {
public func run() async throws {
let keychain = KeychainHelper(id: Constants.keychainID)
let r = Reference.resolveDomain(domain: registry)
try keychain.delete(domain: r)
+3 -2
View File
@@ -17,8 +17,9 @@
import ArgumentParser
extension Application {
struct RegistryCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct RegistryCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "registry",
abstract: "Manage registry configurations",
subcommands: [
+11 -10
View File
@@ -26,8 +26,9 @@ import NIOPosix
import TerminalProgress
extension Application {
struct ContainerRunCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct ContainerRunCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "run",
abstract: "Run a container")
@@ -55,7 +56,7 @@ extension Application {
@Argument(parsing: .captureForPassthrough, help: "Container init process arguments")
var arguments: [String] = []
func run() async throws {
public func run() async throws {
var exitCode: Int32 = 127
let id = Utility.createContainerID(name: self.managementFlags.name)
@@ -166,13 +167,13 @@ extension Application {
}
}
struct ProcessIO {
public struct ProcessIO: Sendable {
let stdin: Pipe?
let stdout: Pipe?
let stderr: Pipe?
var ioTracker: IoTracker?
struct IoTracker {
public struct IoTracker: Sendable {
let stream: AsyncStream<Void>
let cont: AsyncStream<Void>.Continuation
let configuredStreams: Int
@@ -333,7 +334,7 @@ struct ProcessIO {
}
}
public func wait() async throws {
func wait() async throws {
guard let ioTracker = self.ioTracker else {
return
}
@@ -355,10 +356,10 @@ struct ProcessIO {
}
}
struct OSFile: Sendable {
public struct OSFile: Sendable {
private let fd: Int32
enum IOAction: Equatable {
public enum IOAction: Equatable {
case eof
case again
case success
@@ -366,11 +367,11 @@ struct OSFile: Sendable {
case error(_ errno: Int32)
}
init(fd: Int32) {
public init(fd: Int32) {
self.fd = fd
}
init(handle: FileHandle) {
public init(handle: FileHandle) {
self.fd = handle.fileDescriptor
}
+4 -3
View File
@@ -21,8 +21,9 @@ import ContainerizationExtras
import Foundation
extension Application {
struct DNSCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct DNSCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a local DNS domain for containers (must run as an administrator)"
)
@@ -30,7 +31,7 @@ extension Application {
@Argument(help: "the local domain name")
var domainName: String
func run() async throws {
public func run() async throws {
let resolver: HostDNSResolver = HostDNSResolver()
do {
try resolver.createDomain(name: domainName)
+4 -3
View File
@@ -20,8 +20,9 @@ import ContainerizationError
import Foundation
extension Application {
struct DNSDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct DNSDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete a local DNS domain (must run as an administrator)",
aliases: ["rm"]
@@ -30,7 +31,7 @@ extension Application {
@Argument(help: "the local domain name")
var domainName: String
func run() async throws {
public func run() async throws {
let resolver = HostDNSResolver()
do {
try resolver.deleteDomain(name: domainName)
+4 -3
View File
@@ -19,14 +19,15 @@ import ContainerClient
import Foundation
extension Application {
struct DNSList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct DNSList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List local DNS domains",
aliases: ["ls"]
)
func run() async throws {
public func run() async throws {
let resolver: HostDNSResolver = HostDNSResolver()
let domains = resolver.listDomains()
print(domains.joined(separator: "\n"))
+5 -4
View File
@@ -25,8 +25,9 @@ import Foundation
import TerminalProgress
extension Application {
struct KernelSet: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct KernelSet: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "set",
abstract: "Set the default kernel"
)
@@ -46,7 +47,7 @@ extension Application {
@Flag(name: .long, help: "Force install of kernel. If a kernel exists with the same name, it will be overwritten.")
var force: Bool = false
func run() async throws {
public func run() async throws {
if recommended {
let url = DefaultsStore.get(key: .defaultKernelURL)
let path = DefaultsStore.get(key: .defaultKernelBinaryPath)
@@ -100,7 +101,7 @@ extension Application {
}
}
public static func downloadAndInstallWithProgressBar(tarRemoteURL: String, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
static func downloadAndInstallWithProgressBar(tarRemoteURL: String, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
totalTasks: 2
+3 -2
View File
@@ -17,8 +17,9 @@
import ArgumentParser
extension Application {
struct SystemCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "system",
abstract: "Manage system components",
subcommands: [
+3 -2
View File
@@ -19,8 +19,9 @@ import ContainerizationError
import Foundation
extension Application {
struct SystemDNS: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemDNS: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "dns",
abstract: "Manage local DNS domains",
subcommands: [
+3 -2
View File
@@ -17,8 +17,9 @@
import ArgumentParser
extension Application {
struct SystemKernel: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemKernel: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "kernel",
abstract: "Manage the default kernel configuration",
subcommands: [
+6 -4
View File
@@ -22,10 +22,12 @@ import Foundation
import OSLog
extension Application {
struct SystemLogs: AsyncParsableCommand {
static let subsystem = "com.apple.container"
public struct SystemLogs: AsyncParsableCommand {
public init() {}
static let configuration = CommandConfiguration(
public static let subsystem = "com.apple.container"
public static let configuration = CommandConfiguration(
commandName: "logs",
abstract: "Fetch system logs for `container` services"
)
@@ -42,7 +44,7 @@ extension Application {
@Flag(name: .shortAndLong, help: "Follow log output")
var follow: Bool = false
func run() async throws {
public func run() async throws {
let process = Process()
let sigHandler = AsyncSignalHandler.create(notify: [SIGINT, SIGTERM])
+4 -2
View File
@@ -20,8 +20,10 @@ import ContainerizationError
import Foundation
extension Application {
struct SystemProperty: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemProperty: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "property",
abstract: "Manage system property values",
subcommands: [
+5 -4
View File
@@ -23,8 +23,9 @@ import Foundation
import TerminalProgress
extension Application {
struct SystemStart: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemStart: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Start `container` services"
)
@@ -39,7 +40,7 @@ extension Application {
name: .long,
help: "Path to the installation root directory",
transform: { URL(filePath: $0) })
public var installRoot = InstallRoot.defaultURL
var installRoot = InstallRoot.defaultURL
@Flag(name: .long, help: "Enable debug logging for the runtime daemon.")
var debug = false
@@ -49,7 +50,7 @@ extension Application {
help: "Specify whether the default kernel should be installed or not. The default behavior is to prompt the user for a response.")
var kernelInstall: Bool?
func run() async throws {
public func run() async throws {
// Without the true path to the binary in the plist, `container-apiserver` won't launch properly.
// TODO: Use plugin loader for API server.
let executableUrl = CommandLine.executablePathUrl
+4 -3
View File
@@ -22,8 +22,9 @@ import Foundation
import Logging
extension Application {
struct SystemStatus: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct SystemStatus: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "status",
abstract: "Show the status of `container` services"
)
@@ -31,7 +32,7 @@ extension Application {
@Option(name: .shortAndLong, help: "Launchd prefix for `container` services")
var prefix: String = "com.apple.container."
func run() async throws {
public func run() async throws {
let isRegistered = try ServiceManager.isRegistered(fullServiceLabel: "\(prefix)apiserver")
if !isRegistered {
print("apiserver is not running and not registered with launchd")
+5 -3
View File
@@ -22,11 +22,13 @@ import Foundation
import Logging
extension Application {
struct SystemStop: AsyncParsableCommand {
public struct SystemStop: AsyncParsableCommand {
public init() {}
private static let stopTimeoutSeconds: Int32 = 5
private static let shutdownTimeoutSeconds: Int32 = 20
static let configuration = CommandConfiguration(
public static let configuration = CommandConfiguration(
commandName: "stop",
abstract: "Stop all `container` services"
)
@@ -34,7 +36,7 @@ extension Application {
@Option(name: .shortAndLong, help: "Launchd prefix for `container` services")
var prefix: String = "com.apple.container."
func run() async throws {
public func run() async throws {
let log = Logger(
label: "com.apple.container.cli",
factory: { label in
+3 -2
View File
@@ -17,8 +17,9 @@
import ArgumentParser
extension Application {
struct VolumeCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct VolumeCommand: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "volume",
abstract: "Manage container volumes",
subcommands: [
+4 -3
View File
@@ -19,8 +19,9 @@ import ContainerClient
import Foundation
extension Application.VolumeCommand {
struct VolumeCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct VolumeCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a volume"
)
@@ -37,7 +38,7 @@ extension Application.VolumeCommand {
@Argument(help: "Volume name")
var name: String
func run() async throws {
public func run() async throws {
var parsedDriverOpts = Utility.parseKeyValuePairs(driverOpts)
let parsedLabels = Utility.parseKeyValuePairs(labels)
+4 -3
View File
@@ -19,8 +19,9 @@ import ContainerClient
import Foundation
extension Application.VolumeCommand {
struct VolumeDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct VolumeDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Remove one or more volumes",
aliases: ["rm"]
@@ -29,7 +30,7 @@ extension Application.VolumeCommand {
@Argument(help: "Volume name(s)")
var names: [String]
func run() async throws {
public func run() async throws {
for name in names {
try await ClientVolume.delete(name: name)
print(name)
+4 -3
View File
@@ -19,8 +19,9 @@ import ContainerClient
import Foundation
extension Application.VolumeCommand {
struct VolumeInspect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct VolumeInspect: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "inspect",
abstract: "Display detailed information on one or more volumes"
)
@@ -28,7 +29,7 @@ extension Application.VolumeCommand {
@Argument(help: "Volume name(s)")
var names: [String]
func run() async throws {
public func run() async throws {
var volumes: [Volume] = []
for name in names {
+5 -4
View File
@@ -20,8 +20,9 @@ import ContainerizationExtras
import Foundation
extension Application.VolumeCommand {
struct VolumeList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct VolumeList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List volumes",
aliases: ["ls"]
@@ -33,7 +34,7 @@ extension Application.VolumeCommand {
@Option(name: .long, help: "Format of the output")
var format: Application.ListFormat = .table
func run() async throws {
public func run() async throws {
let volumes = try await ClientVolume.list()
try printVolumes(volumes: volumes, format: format)
}
@@ -42,7 +43,7 @@ extension Application.VolumeCommand {
[["NAME", "DRIVER", "OPTIONS"]]
}
private func printVolumes(volumes: [Volume], format: Application.ListFormat) throws {
func printVolumes(volumes: [Volume], format: Application.ListFormat) throws {
if format == .json {
let data = try JSONEncoder().encode(volumes)
print(String(data: data, encoding: .utf8)!)
+39
View File
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 ArgumentParser
import ContainerClient
import ContainerCommands
@main
public struct Executable: AsyncParsableCommand {
public init() {}
@Argument(parsing: .captureForPassthrough)
var arguments: [String] = []
public static let configuration = Application.configuration
public static func main() async throws {
try await Application.main()
}
public func run() async throws {
var application = try Application.parse(arguments)
try application.validate()
try application.run()
}
}