diff --git a/Package.swift b/Package.swift index 8a738ffd..ae5b989a 100644 --- a/Package.swift +++ b/Package.swift @@ -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"), diff --git a/Sources/CLI/Application.swift b/Sources/CLI/Application.swift index 8f60c88c..c04656f9 100644 --- a/Sources/CLI/Application.swift +++ b/Sources/CLI/Application.swift @@ -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 } diff --git a/Sources/CLI/BuildCommand.swift b/Sources/CLI/BuildCommand.swift index cc1bb3e6..ab6bcf66 100644 --- a/Sources/CLI/BuildCommand.swift +++ b/Sources/CLI/BuildCommand.swift @@ -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)") } diff --git a/Sources/CLI/Builder/Builder.swift b/Sources/CLI/Builder/Builder.swift index ad9eb6c9..d5915ab8 100644 --- a/Sources/CLI/Builder/Builder.swift +++ b/Sources/CLI/Builder/Builder.swift @@ -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: [ diff --git a/Sources/CLI/Builder/BuilderDelete.swift b/Sources/CLI/Builder/BuilderDelete.swift index 8074fd60..252d54cc 100644 --- a/Sources/CLI/Builder/BuilderDelete.swift +++ b/Sources/CLI/Builder/BuilderDelete.swift @@ -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 { diff --git a/Sources/CLI/Builder/BuilderStart.swift b/Sources/CLI/Builder/BuilderStart.swift index 95aafecf..e03af8fb 100644 --- a/Sources/CLI/Builder/BuilderStart.swift +++ b/Sources/CLI/Builder/BuilderStart.swift @@ -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, diff --git a/Sources/CLI/Builder/BuilderStatus.swift b/Sources/CLI/Builder/BuilderStatus.swift index b1210a3d..747ee210 100644 --- a/Sources/CLI/Builder/BuilderStatus.swift +++ b/Sources/CLI/Builder/BuilderStatus.swift @@ -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 { diff --git a/Sources/CLI/Builder/BuilderStop.swift b/Sources/CLI/Builder/BuilderStop.swift index e7484c9c..ae46474c 100644 --- a/Sources/CLI/Builder/BuilderStop.swift +++ b/Sources/CLI/Builder/BuilderStop.swift @@ -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() diff --git a/Sources/CLI/Container/ContainerCreate.swift b/Sources/CLI/Container/ContainerCreate.swift index 8fd96cd5..5375f621 100644 --- a/Sources/CLI/Container/ContainerCreate.swift +++ b/Sources/CLI/Container/ContainerCreate.swift @@ -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, diff --git a/Sources/CLI/Container/ContainerDelete.swift b/Sources/CLI/Container/ContainerDelete.swift index dd7707d8..63794da4 100644 --- a/Sources/CLI/Container/ContainerDelete.swift +++ b/Sources/CLI/Container/ContainerDelete.swift @@ -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(containerIDs) var containers = [ClientContainer]() diff --git a/Sources/CLI/Container/ContainerExec.swift b/Sources/CLI/Container/ContainerExec.swift index dc4a92ae..123a39ab 100644 --- a/Sources/CLI/Container/ContainerExec.swift +++ b/Sources/CLI/Container/ContainerExec.swift @@ -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) diff --git a/Sources/CLI/Container/ContainerInspect.swift b/Sources/CLI/Container/ContainerInspect.swift index 43bda51a..8de6dbfe 100644 --- a/Sources/CLI/Container/ContainerInspect.swift +++ b/Sources/CLI/Container/ContainerInspect.swift @@ -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 { diff --git a/Sources/CLI/Container/ContainerKill.swift b/Sources/CLI/Container/ContainerKill.swift index 9b9ef4ed..16dd3bb5 100644 --- a/Sources/CLI/Container/ContainerKill.swift +++ b/Sources/CLI/Container/ContainerKill.swift @@ -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(containerIDs) var containers = try await ClientContainer.list().filter { c in diff --git a/Sources/CLI/Container/ContainerList.swift b/Sources/CLI/Container/ContainerList.swift index 43e5a4ce..403e7a50 100644 --- a/Sources/CLI/Container/ContainerList.swift +++ b/Sources/CLI/Container/ContainerList.swift @@ -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) } diff --git a/Sources/CLI/Container/ContainerLogs.swift b/Sources/CLI/Container/ContainerLogs.swift index 5f119966..cc60c29d 100644 --- a/Sources/CLI/Container/ContainerLogs.swift +++ b/Sources/CLI/Container/ContainerLogs.swift @@ -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() diff --git a/Sources/CLI/Container/ContainerStart.swift b/Sources/CLI/Container/ContainerStart.swift index 4f8a86cf..7369c080 100644 --- a/Sources/CLI/Container/ContainerStart.swift +++ b/Sources/CLI/Container/ContainerStart.swift @@ -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( diff --git a/Sources/CLI/Container/ContainerStop.swift b/Sources/CLI/Container/ContainerStop.swift index 78f69090..3bfbca8c 100644 --- a/Sources/CLI/Container/ContainerStop.swift +++ b/Sources/CLI/Container/ContainerStop.swift @@ -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(containerIDs) var containers = [ClientContainer]() if self.all { diff --git a/Sources/CLI/Container/ContainersCommand.swift b/Sources/CLI/Container/ContainersCommand.swift index ef6aff93..05abcd06 100644 --- a/Sources/CLI/Container/ContainersCommand.swift +++ b/Sources/CLI/Container/ContainersCommand.swift @@ -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: [ diff --git a/Sources/CLI/DefaultCommand.swift b/Sources/CLI/DefaultCommand.swift index 20a321f1..5b08c59e 100644 --- a/Sources/CLI/DefaultCommand.swift +++ b/Sources/CLI/DefaultCommand.swift @@ -21,7 +21,7 @@ import Darwin import Foundation struct DefaultCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( commandName: nil, shouldDisplay: false ) diff --git a/Sources/CLI/Image/ImageCommand.swift b/Sources/CLI/Image/ImageCommand.swift index 7aa74d99..2db4cf8e 100644 --- a/Sources/CLI/Image/ImageCommand.swift +++ b/Sources/CLI/Image/ImageCommand.swift @@ -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: [ diff --git a/Sources/CLI/Image/ImageInspect.swift b/Sources/CLI/Image/ImageInspect.swift index cea35686..a033d563 100644 --- a/Sources/CLI/Image/ImageInspect.swift +++ b/Sources/CLI/Image/ImageInspect.swift @@ -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 diff --git a/Sources/CLI/Image/ImageList.swift b/Sources/CLI/Image/ImageList.swift index bacf9cac..e6b97d9b 100644 --- a/Sources/CLI/Image/ImageList.swift +++ b/Sources/CLI/Image/ImageList.swift @@ -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) } diff --git a/Sources/CLI/Image/ImageLoad.swift b/Sources/CLI/Image/ImageLoad.swift index 719fd19e..6b7fdd90 100644 --- a/Sources/CLI/Image/ImageLoad.swift +++ b/Sources/CLI/Image/ImageLoad.swift @@ -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)) diff --git a/Sources/CLI/Image/ImagePrune.swift b/Sources/CLI/Image/ImagePrune.swift index d233247f..290ce91e 100644 --- a/Sources/CLI/Image/ImagePrune.swift +++ b/Sources/CLI/Image/ImagePrune.swift @@ -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)) diff --git a/Sources/CLI/Image/ImagePull.swift b/Sources/CLI/Image/ImagePull.swift index d6e5a000..4d9275f5 100644 --- a/Sources/CLI/Image/ImagePull.swift +++ b/Sources/CLI/Image/ImagePull.swift @@ -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) diff --git a/Sources/CLI/Image/ImagePush.swift b/Sources/CLI/Image/ImagePush.swift index e61d162d..792f79ba 100644 --- a/Sources/CLI/Image/ImagePush.swift +++ b/Sources/CLI/Image/ImagePush.swift @@ -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) diff --git a/Sources/CLI/Image/ImageRemove.swift b/Sources/CLI/Image/ImageRemove.swift index 2f0c86c2..d182ad7a 100644 --- a/Sources/CLI/Image/ImageRemove.swift +++ b/Sources/CLI/Image/ImageRemove.swift @@ -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) } } diff --git a/Sources/CLI/Image/ImageSave.swift b/Sources/CLI/Image/ImageSave.swift index e1f31efc..d6d4063e 100644 --- a/Sources/CLI/Image/ImageSave.swift +++ b/Sources/CLI/Image/ImageSave.swift @@ -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) diff --git a/Sources/CLI/Image/ImageTag.swift b/Sources/CLI/Image/ImageTag.swift index 01a76190..f28e5b45 100644 --- a/Sources/CLI/Image/ImageTag.swift +++ b/Sources/CLI/Image/ImageTag.swift @@ -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) diff --git a/Sources/CLI/Network/NetworkCommand.swift b/Sources/CLI/Network/NetworkCommand.swift index 7e502431..b2d0797c 100644 --- a/Sources/CLI/Network/NetworkCommand.swift +++ b/Sources/CLI/Network/NetworkCommand.swift @@ -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: [ diff --git a/Sources/CLI/Network/NetworkCreate.swift b/Sources/CLI/Network/NetworkCreate.swift index e07f5fce..a759eea5 100644 --- a/Sources/CLI/Network/NetworkCreate.swift +++ b/Sources/CLI/Network/NetworkCreate.swift @@ -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) diff --git a/Sources/CLI/Network/NetworkDelete.swift b/Sources/CLI/Network/NetworkDelete.swift index 431f53eb..ac3aa02b 100644 --- a/Sources/CLI/Network/NetworkDelete.swift +++ b/Sources/CLI/Network/NetworkDelete.swift @@ -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(networkNames) let networks: [NetworkState] diff --git a/Sources/CLI/Network/NetworkInspect.swift b/Sources/CLI/Network/NetworkInspect.swift index 614c8b11..3c4b918e 100644 --- a/Sources/CLI/Network/NetworkInspect.swift +++ b/Sources/CLI/Network/NetworkInspect.swift @@ -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 { diff --git a/Sources/CLI/Network/NetworkList.swift b/Sources/CLI/Network/NetworkList.swift index 2c1dcddc..155e90bc 100644 --- a/Sources/CLI/Network/NetworkList.swift +++ b/Sources/CLI/Network/NetworkList.swift @@ -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 { diff --git a/Sources/CLI/Registry/Login.swift b/Sources/CLI/Registry/Login.swift index 7de7fe7e..3346f3fe 100644 --- a/Sources/CLI/Registry/Login.swift +++ b/Sources/CLI/Registry/Login.swift @@ -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 { diff --git a/Sources/CLI/Registry/Logout.swift b/Sources/CLI/Registry/Logout.swift index a24996e1..ac115501 100644 --- a/Sources/CLI/Registry/Logout.swift +++ b/Sources/CLI/Registry/Logout.swift @@ -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) diff --git a/Sources/CLI/Registry/RegistryCommand.swift b/Sources/CLI/Registry/RegistryCommand.swift index c946bb87..55c2fc1e 100644 --- a/Sources/CLI/Registry/RegistryCommand.swift +++ b/Sources/CLI/Registry/RegistryCommand.swift @@ -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: [ diff --git a/Sources/CLI/RunCommand.swift b/Sources/CLI/RunCommand.swift index ef116b28..5fcc47b8 100644 --- a/Sources/CLI/RunCommand.swift +++ b/Sources/CLI/RunCommand.swift @@ -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 let cont: AsyncStream.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 } diff --git a/Sources/CLI/System/DNS/DNSCreate.swift b/Sources/CLI/System/DNS/DNSCreate.swift index 2dbe2d8a..22e27401 100644 --- a/Sources/CLI/System/DNS/DNSCreate.swift +++ b/Sources/CLI/System/DNS/DNSCreate.swift @@ -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) diff --git a/Sources/CLI/System/DNS/DNSDelete.swift b/Sources/CLI/System/DNS/DNSDelete.swift index b3360bb5..689254e8 100644 --- a/Sources/CLI/System/DNS/DNSDelete.swift +++ b/Sources/CLI/System/DNS/DNSDelete.swift @@ -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) diff --git a/Sources/CLI/System/DNS/DNSList.swift b/Sources/CLI/System/DNS/DNSList.swift index 61641577..87e66933 100644 --- a/Sources/CLI/System/DNS/DNSList.swift +++ b/Sources/CLI/System/DNS/DNSList.swift @@ -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")) diff --git a/Sources/CLI/System/Kernel/KernelSet.swift b/Sources/CLI/System/Kernel/KernelSet.swift index 2fe18561..4fd3df39 100644 --- a/Sources/CLI/System/Kernel/KernelSet.swift +++ b/Sources/CLI/System/Kernel/KernelSet.swift @@ -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 diff --git a/Sources/CLI/System/SystemCommand.swift b/Sources/CLI/System/SystemCommand.swift index 2814baf0..92bf8b1c 100644 --- a/Sources/CLI/System/SystemCommand.swift +++ b/Sources/CLI/System/SystemCommand.swift @@ -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: [ diff --git a/Sources/CLI/System/SystemDNS.swift b/Sources/CLI/System/SystemDNS.swift index 6c3840cf..9e253370 100644 --- a/Sources/CLI/System/SystemDNS.swift +++ b/Sources/CLI/System/SystemDNS.swift @@ -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: [ diff --git a/Sources/CLI/System/SystemKernel.swift b/Sources/CLI/System/SystemKernel.swift index 942bd696..c44e3a2d 100644 --- a/Sources/CLI/System/SystemKernel.swift +++ b/Sources/CLI/System/SystemKernel.swift @@ -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: [ diff --git a/Sources/CLI/System/SystemLogs.swift b/Sources/CLI/System/SystemLogs.swift index e2b87ffb..c1f606a8 100644 --- a/Sources/CLI/System/SystemLogs.swift +++ b/Sources/CLI/System/SystemLogs.swift @@ -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]) diff --git a/Sources/CLI/System/SystemProperty.swift b/Sources/CLI/System/SystemProperty.swift index 4404f565..97c09ac4 100644 --- a/Sources/CLI/System/SystemProperty.swift +++ b/Sources/CLI/System/SystemProperty.swift @@ -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: [ diff --git a/Sources/CLI/System/SystemStart.swift b/Sources/CLI/System/SystemStart.swift index 8b6d6d52..929bb606 100644 --- a/Sources/CLI/System/SystemStart.swift +++ b/Sources/CLI/System/SystemStart.swift @@ -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 diff --git a/Sources/CLI/System/SystemStatus.swift b/Sources/CLI/System/SystemStatus.swift index cf4a86fc..c46e28b8 100644 --- a/Sources/CLI/System/SystemStatus.swift +++ b/Sources/CLI/System/SystemStatus.swift @@ -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") diff --git a/Sources/CLI/System/SystemStop.swift b/Sources/CLI/System/SystemStop.swift index 32824dd0..b3179b3b 100644 --- a/Sources/CLI/System/SystemStop.swift +++ b/Sources/CLI/System/SystemStop.swift @@ -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 diff --git a/Sources/CLI/Volume/VolumeCommand.swift b/Sources/CLI/Volume/VolumeCommand.swift index fe429369..f4e0f426 100644 --- a/Sources/CLI/Volume/VolumeCommand.swift +++ b/Sources/CLI/Volume/VolumeCommand.swift @@ -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: [ diff --git a/Sources/CLI/Volume/VolumeCreate.swift b/Sources/CLI/Volume/VolumeCreate.swift index a9accb95..928abbd9 100644 --- a/Sources/CLI/Volume/VolumeCreate.swift +++ b/Sources/CLI/Volume/VolumeCreate.swift @@ -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) diff --git a/Sources/CLI/Volume/VolumeDelete.swift b/Sources/CLI/Volume/VolumeDelete.swift index 5dddab7e..80e3408d 100644 --- a/Sources/CLI/Volume/VolumeDelete.swift +++ b/Sources/CLI/Volume/VolumeDelete.swift @@ -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) diff --git a/Sources/CLI/Volume/VolumeInspect.swift b/Sources/CLI/Volume/VolumeInspect.swift index 1fe08b01..f6405e24 100644 --- a/Sources/CLI/Volume/VolumeInspect.swift +++ b/Sources/CLI/Volume/VolumeInspect.swift @@ -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 { diff --git a/Sources/CLI/Volume/VolumeList.swift b/Sources/CLI/Volume/VolumeList.swift index 7bd3ce26..840df9ba 100644 --- a/Sources/CLI/Volume/VolumeList.swift +++ b/Sources/CLI/Volume/VolumeList.swift @@ -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)!) diff --git a/Sources/ExecutableCLI/Executable.swift b/Sources/ExecutableCLI/Executable.swift new file mode 100644 index 00000000..2fd5485f --- /dev/null +++ b/Sources/ExecutableCLI/Executable.swift @@ -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() + } +}