diff --git a/Sources/CLI/Executable.swift b/Sources/CLI/ContainerCLI.swift similarity index 96% rename from Sources/CLI/Executable.swift rename to Sources/CLI/ContainerCLI.swift index 2fd5485f..62eec52d 100644 --- a/Sources/CLI/Executable.swift +++ b/Sources/CLI/ContainerCLI.swift @@ -19,7 +19,7 @@ import ContainerClient import ContainerCommands @main -public struct Executable: AsyncParsableCommand { +public struct ContainerCLI: AsyncParsableCommand { public init() {} @Argument(parsing: .captureForPassthrough) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index a2b081ca..4d1d21fb 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -29,43 +29,49 @@ public struct Flags { public struct Process: ParsableArguments { public init() {} - @Option( - name: [.customLong("cwd"), .customShort("w"), .customLong("workdir")], - help: "Current working directory for the container") - public var cwd: String? - - @Option(name: [.customLong("env"), .customShort("e")], help: "Set environment variables") + @Option(name: .shortAndLong, help: "Set environment variables (format: key=value)") public var env: [String] = [] - @Option(name: .customLong("env-file"), help: "Read in a file of environment variables") + @Option( + name: .long, + help: "Read in a file of environment variables (key=value format, ignores # comments and blank lines)" + ) public var envFile: [String] = [] - @Option(name: .customLong("uid"), help: "Set the uid for the process") - public var uid: UInt32? - - @Option(name: .customLong("gid"), help: "Set the gid for the process") + @Option(name: .long, help: "Set the group ID for the process") public var gid: UInt32? - @Flag(name: [.customLong("interactive"), .customShort("i")], help: "Keep Stdin open even if not attached") + @Flag(name: .shortAndLong, help: "Keep the standard input open even if not attached") public var interactive = false - @Flag(name: [.customLong("tty"), .customShort("t")], help: "Open a tty with the process") + @Flag(name: .shortAndLong, help: "Open a TTY with the process") public var tty = false - @Option(name: [.customLong("user"), .customShort("u")], help: "Set the user for the process") + @Option(name: .shortAndLong, help: "Set the user for the process (format: name|uid[:gid])") public var user: String? + + @Option(name: .long, help: "Set the user ID for the process") + public var uid: UInt32? + + @Option( + name: [.customShort("w"), .customLong("workdir"), .long], + help: .init( + "Set the initial working directory inside the container", + valueName: "dir" + ) + ) + public var cwd: String? } public struct Resource: ParsableArguments { public init() {} - @Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container") + @Option(name: .shortAndLong, help: "Number of CPUs to allocate to the container") public var cpus: Int64? @Option( - name: [.customLong("memory"), .customShort("m")], - help: - "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix" + name: .shortAndLong, + help: "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix" ) public var memory: String? } @@ -84,83 +90,115 @@ public struct Flags { public struct Management: ParsableArguments { public init() {} - @Flag(name: [.customLong("detach"), .short], help: "Run the container and detach from the process") - public var detach = false - - @Option(name: .customLong("entrypoint"), help: "Override the entrypoint of the image") - public var entryPoint: String? - - @Option(name: .customLong("mount"), help: "Add a mount to the container (type=<>,source=<>,target=<>,readonly)") - public var mounts: [String] = [] - - @Option(name: [.customLong("publish"), .short], help: "Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])") - public var publishPorts: [String] = [] - - @Option(name: .customLong("publish-socket"), help: "Publish a socket from container to host (format: host_path:container_path)") - public var publishSockets: [String] = [] - - @Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path") - public var tmpFs: [String] = [] - - @Option(name: .customLong("name"), help: "Assign a name to the container. If excluded will be a generated UUID") - public var name: String? - - @Flag(name: [.customLong("remove"), .customLong("rm")], help: "Remove the container after it stops") - public var remove = false - - @Option(name: .customLong("platform"), help: "Platform for the image if it's multi-platform. This takes precedence over --os and --arch") - public var platform: String? - - @Option(name: .customLong("os"), help: "Set OS if image can target multiple operating systems") - public var os = "linux" - - @Option( - name: [.long, .short], help: "Set arch if image can target multiple architectures") + @Option(name: .shortAndLong, help: "Set arch if image can target multiple architectures") public var arch: String = Arch.hostArchitecture().rawValue - @Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container") - public var volumes: [String] = [] + @Option(name: .long, help: "Write the container ID to the path provided") + public var cidfile = "" + + @Flag(name: .shortAndLong, help: "Run the container and detach from the process") + public var detach = false @Option( - name: [.customLong("kernel"), .short], help: "Set a custom kernel path", completion: .file(), + name: .customLong("dns"), + help: .init("DNS nameserver IP address", valueName: "ip") + ) + public var dnsNameservers: [String] = [] + + @Option( + name: .long, + help: .init("Default DNS domain", valueName: "domain") + ) + public var dnsDomain: String? = nil + + @Option( + name: .customLong("dns-option"), + help: .init("DNS options", valueName: "option") + ) + public var dnsOptions: [String] = [] + + @Option( + name: .customLong("dns-search"), + help: .init("DNS search domains", valueName: "domain") + ) + public var dnsSearchDomains: [String] = [] + + @Option( + name: .long, + help: .init( + "Override the entrypoint of the image", + valueName: "cmd" + ) + ) + public var entrypoint: String? + + @Option( + name: .shortAndLong, + help: .init("Set a custom kernel path", valueName: "path"), + completion: .file(), transform: { str in URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false) - }) + } + ) public var kernel: String? + @Option(name: [.short, .customLong("label")], help: "Add a key=value label to the container") + public var labels: [String] = [] + + @Option(name: .customLong("mount"), help: "Add a mount to the container (format: type=<>,source=<>,target=<>,readonly)") + public var mounts: [String] = [] + + @Option(name: .long, help: "Use the specified name as the container ID") + public var name: String? + @Option(name: [.customLong("network")], help: "Attach the container to a network") public var networks: [String] = [] - @Option(name: .customLong("cidfile"), help: "Write the container ID to the path provided") - public var cidfile = "" - @Flag(name: [.customLong("no-dns")], help: "Do not configure DNS in the container") public var dnsDisabled = false - @Option(name: .customLong("dns"), help: "DNS nameserver IP address") - public var dnsNameservers: [String] = [] + @Option(name: .long, help: "Set OS if image can target multiple operating systems") + public var os = "linux" - @Option(name: .customLong("dns-domain"), help: "Default DNS domain") - public var dnsDomain: String? = nil + @Option( + name: [.customShort("p"), .customLong("publish")], + help: .init( + "Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])", + valueName: "spec" + ) + ) + public var publishPorts: [String] = [] - @Option(name: .customLong("dns-search"), help: "DNS search domains") - public var dnsSearchDomains: [String] = [] + @Option(name: .long, help: "Platform for the image if it's multi-platform. This takes precedence over --os and --arch") + public var platform: String? - @Option(name: .customLong("dns-option"), help: "DNS options") - public var dnsOptions: [String] = [] + @Option( + name: .customLong("publish-socket"), + help: .init( + "Publish a socket from container to host (format: host_path:container_path)", + valueName: "spec" + ) + ) + public var publishSockets: [String] = [] - @Option(name: [.customLong("label"), .short], help: "Add a key=value label to the container") - public var labels: [String] = [] + @Flag(name: [.customLong("rm"), .long], help: "Remove the container after it stops") + public var remove = false + + @Flag(name: .long, help: "Forward SSH agent socket to container") + public var ssh = false + + @Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path") + public var tmpFs: [String] = [] + + @Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container") + public var volumes: [String] = [] @Flag( - name: [.customLong("virtualization")], + name: .long, help: - "Expose virtualization capabilities to the container. (Host must have nested virtualization support, and guest kernel must have virtualization capabilities enabled)" + "Expose virtualization capabilities to the container (requires host and guest support)" ) public var virtualization: Bool = false - - @Flag(name: .customLong("ssh"), help: "Forward SSH agent socket to container") - public var ssh = false } public struct Progress: ParsableArguments { @@ -170,7 +208,7 @@ public struct Flags { self.disableProgressUpdates = disableProgressUpdates } - @Flag(name: .customLong("disable-progress-updates"), help: "Disable progress bar updates") + @Flag(name: .long, help: "Disable progress bar updates") public var disableProgressUpdates = false } } diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 493bf9fb..24f84493 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -192,7 +192,7 @@ public struct Parser { var result: [String] = [] var hasEntrypointOverride: Bool = false // ensure the entrypoint is honored if it has been explicitly set by the user - if let entrypoint = managementFlags.entryPoint, !entrypoint.isEmpty { + if let entrypoint = managementFlags.entrypoint, !entrypoint.isEmpty { result = [entrypoint] hasEntrypointOverride = true } else if let entrypoint = config?.entrypoint, !entrypoint.isEmpty { diff --git a/Sources/ContainerCommands/Application.swift b/Sources/ContainerCommands/Application.swift index c04656f9..10ea3404 100644 --- a/Sources/ContainerCommands/Application.swift +++ b/Sources/ContainerCommands/Application.swift @@ -59,7 +59,7 @@ public struct Application: AsyncParsableCommand { ContainerKill.self, ContainerList.self, ContainerLogs.self, - ContainerRunCommand.self, + ContainerRun.self, ContainerStart.self, ContainerStop.self, ] diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index ec44fdc9..affa786c 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -38,36 +38,18 @@ extension Application { return config } - @Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container") - var cpus: Int64 = 2 - @Option( - name: [.customLong("memory"), .customShort("m")], - help: - "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix" + name: .shortAndLong, + help: ArgumentHelp("Add the architecture type to the build", valueName: "value"), + transform: { val in val.split(separator: ",").map { String($0) } } ) - var memory: String = "2048MB" + var arch: [[String]] = { + [[Arch.hostArchitecture().rawValue]] + }() @Option(name: .long, help: ArgumentHelp("Set build-time variables", valueName: "key=val")) var buildArg: [String] = [] - @Argument(help: "Build directory") - var contextDir: String = "." - - @Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path")) - var file: String = "Dockerfile" - - @Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val")) - var label: [String] = [] - - @Flag(name: .long, help: "Do not use cache") - var noCache: Bool = false - - @Option(name: .shortAndLong, help: ArgumentHelp("Output configuration for the build", valueName: "value")) - var output: [String] = { - ["type=oci"] - }() - @Option(name: .long, help: ArgumentHelp("Cache imports for the build", valueName: "value", visibility: .hidden)) var cacheIn: [String] = { [] @@ -78,16 +60,33 @@ extension Application { [] }() + @Option(name: .shortAndLong, help: "Number of CPUs to allocate to the builder container") + var cpus: Int64 = 2 + + @Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path")) + var file: String = "Dockerfile" + + @Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val")) + var label: [String] = [] + @Option( - name: .long, - help: "add the platform to the build", - transform: { val in val.split(separator: ",").map { String($0) } } + name: .shortAndLong, + help: + "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix" ) - var platform: [[String]] = [[]] + var memory: String = "2048MB" + + @Flag(name: .long, help: "Do not use cache") + var noCache: Bool = false + + @Option(name: .shortAndLong, help: ArgumentHelp("Output configuration for the build (format: type=[,dest=])", valueName: "value")) + var output: [String] = { + ["type=oci"] + }() @Option( name: .long, - help: ArgumentHelp("add the OS type to the build", valueName: "value"), + help: ArgumentHelp("Add the OS type to the build", valueName: "value"), transform: { val in val.split(separator: ",").map { String($0) } } ) var os: [[String]] = { @@ -95,28 +94,29 @@ extension Application { }() @Option( - name: [.long, .short], - help: ArgumentHelp("add the architecture type to the build", valueName: "value"), + name: .long, + help: "Add the platform to the build (takes precedence over --os and --arch)", transform: { val in val.split(separator: ",").map { String($0) } } ) - var arch: [[String]] = { - [[Arch.hostArchitecture().rawValue]] - }() + var platform: [[String]] = [[]] - @Option(name: .long, help: ArgumentHelp("Progress type - one of [auto|plain|tty]", valueName: "type")) + @Option(name: .long, help: ArgumentHelp("Progress type (format: auto|plain|tty)]", valueName: "type")) var progress: String = "auto" - @Option(name: .long, help: ArgumentHelp("Builder-shim vsock port", valueName: "port")) - var vsockPort: UInt32 = 8088 + @Flag(name: .shortAndLong, help: "Suppress build output") + var quiet: Bool = false - @Option(name: [.customShort("t"), .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name")) + @Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name")) var targetImageName: String = UUID().uuidString.lowercased() @Option(name: .long, help: ArgumentHelp("Set the target build stage", valueName: "stage")) var target: String = "" - @Flag(name: .shortAndLong, help: "Suppress build output") - var quiet: Bool = false + @Option(name: .long, help: ArgumentHelp("Builder shim vsock port", valueName: "port")) + var vsockPort: UInt32 = 8088 + + @Argument(help: "Build directory") + var contextDir: String = "." public func run() async throws { do { @@ -257,7 +257,7 @@ extension Application { return results }() group.addTask { [terminal, buildArg, contextDir, label, noCache, target, quiet, cacheIn, cacheOut] in - let config = ContainerBuild.Builder.BuildConfig( + let config = Builder.BuildConfig( buildID: buildID, contentStore: RemoteContentStoreClient(), buildArgs: buildArg, diff --git a/Sources/ContainerCommands/Container/ContainerCreate.swift b/Sources/ContainerCommands/Container/ContainerCreate.swift index 5375f621..283b4312 100644 --- a/Sources/ContainerCommands/Container/ContainerCreate.swift +++ b/Sources/ContainerCommands/Container/ContainerCreate.swift @@ -28,27 +28,27 @@ extension Application { commandName: "create", abstract: "Create a new container") + @OptionGroup(title: "Process options") + var processFlags: Flags.Process + + @OptionGroup(title: "Resource options") + var resourceFlags: Flags.Resource + + @OptionGroup(title: "Management options") + var managementFlags: Flags.Management + + @OptionGroup(title: "Registry options") + var registryFlags: Flags.Registry + + @OptionGroup + var global: Flags.Global + @Argument(help: "Image name") var image: String @Argument(parsing: .captureForPassthrough, help: "Container init process arguments") var arguments: [String] = [] - @OptionGroup - var processFlags: Flags.Process - - @OptionGroup - var resourceFlags: Flags.Resource - - @OptionGroup - var managementFlags: Flags.Management - - @OptionGroup - var registryFlags: Flags.Registry - - @OptionGroup - var global: Flags.Global - public func run() async throws { let progressConfig = try ProgressConfig( showTasks: true, diff --git a/Sources/ContainerCommands/Container/ContainerDelete.swift b/Sources/ContainerCommands/Container/ContainerDelete.swift index 3719c04d..d0826d5c 100644 --- a/Sources/ContainerCommands/Container/ContainerDelete.swift +++ b/Sources/ContainerCommands/Container/ContainerDelete.swift @@ -28,23 +28,23 @@ extension Application { abstract: "Delete one or more containers", aliases: ["rm"]) - @Flag(name: .shortAndLong, help: "Delete containers even if they are running") - var force = false - @Flag(name: .shortAndLong, help: "Remove all containers") var all = false + @Flag(name: .shortAndLong, help: "Delete containers even if they are running") + var force = false + @OptionGroup var global: Flags.Global - @Argument(help: "Container IDs/names") - var containerIDs: [String] = [] + @Argument(help: "Container IDs") + var containerIds: [String] = [] public func validate() throws { - if containerIDs.count == 0 && !all { + if containerIds.count == 0 && !all { throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied") } - if containerIDs.count > 0 && all { + if containerIds.count > 0 && all { throw ContainerizationError( .invalidArgument, message: "explicitly supplied container ID(s) conflict with the --all flag" @@ -53,7 +53,7 @@ extension Application { } public mutating func run() async throws { - let set = Set(containerIDs) + let set = Set(containerIds) var containers = [ClientContainer]() if all { diff --git a/Sources/ContainerCommands/Container/ContainerExec.swift b/Sources/ContainerCommands/Container/ContainerExec.swift index 123a39ab..03e9e575 100644 --- a/Sources/ContainerCommands/Container/ContainerExec.swift +++ b/Sources/ContainerCommands/Container/ContainerExec.swift @@ -28,21 +28,21 @@ extension Application { commandName: "exec", abstract: "Run a new command in a running container") - @OptionGroup + @OptionGroup(title: "Process options") var processFlags: Flags.Process @OptionGroup var global: Flags.Global - @Argument(help: "Running containers ID") - var containerID: String + @Argument(help: "Container ID") + var containerId: String @Argument(parsing: .captureForPassthrough, help: "New process arguments") var arguments: [String] public func run() async throws { var exitCode: Int32 = 127 - let container = try await ClientContainer.get(id: containerID) + let container = try await ClientContainer.get(id: containerId) try ensureRunning(container: container) let stdin = self.processFlags.interactive diff --git a/Sources/ContainerCommands/Container/ContainerInspect.swift b/Sources/ContainerCommands/Container/ContainerInspect.swift index 8de6dbfe..36b23c36 100644 --- a/Sources/ContainerCommands/Container/ContainerInspect.swift +++ b/Sources/ContainerCommands/Container/ContainerInspect.swift @@ -30,12 +30,12 @@ extension Application { @OptionGroup var global: Flags.Global - @Argument(help: "Containers to inspect") - var containers: [String] + @Argument(help: "Container IDs") + var containerIds: [String] public func run() async throws { let objects: [any Codable] = try await ClientContainer.list().filter { - containers.contains($0.id) + containerIds.contains($0.id) }.map { PrintableContainer($0) } diff --git a/Sources/ContainerCommands/Container/ContainerKill.swift b/Sources/ContainerCommands/Container/ContainerKill.swift index 16dd3bb5..b9e4446e 100644 --- a/Sources/ContainerCommands/Container/ContainerKill.swift +++ b/Sources/ContainerCommands/Container/ContainerKill.swift @@ -26,31 +26,31 @@ extension Application { public static let configuration = CommandConfiguration( commandName: "kill", - abstract: "Kill one or more running containers") + abstract: "Kill or signal one or more running containers") - @Option(name: .shortAndLong, help: "Signal to send the container(s)") - var signal: String = "KILL" - - @Flag(name: .shortAndLong, help: "Kill all running containers") + @Flag(name: .shortAndLong, help: "Kill or signal all running containers") var all = false - @Argument(help: "Container IDs") - var containerIDs: [String] = [] + @Option(name: .shortAndLong, help: "Signal to send to the container(s)") + var signal: String = "KILL" @OptionGroup var global: Flags.Global + @Argument(help: "Container IDs") + var containerIds: [String] = [] + public func validate() throws { - if containerIDs.count == 0 && !all { + if containerIds.count == 0 && !all { throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied") } - if containerIDs.count > 0 && all { + if containerIds.count > 0 && all { throw ContainerizationError(.invalidArgument, message: "explicitly supplied container IDs conflicts with the --all flag") } } public mutating func run() async throws { - let set = Set(containerIDs) + let set = Set(containerIds) var containers = try await ClientContainer.list().filter { c in c.status == .running diff --git a/Sources/ContainerCommands/Container/ContainerLogs.swift b/Sources/ContainerCommands/Container/ContainerLogs.swift index cc60c29d..f2bf468f 100644 --- a/Sources/ContainerCommands/Container/ContainerLogs.swift +++ b/Sources/ContainerCommands/Container/ContainerLogs.swift @@ -27,27 +27,27 @@ extension Application { public static let configuration = CommandConfiguration( commandName: "logs", - abstract: "Fetch container stdio or boot logs" + abstract: "Fetch container logs" ) - @OptionGroup - var global: Flags.Global - - @Flag(name: .shortAndLong, help: "Follow log output") - var follow: Bool = false - @Flag(name: .long, help: "Display the boot log for the container instead of stdio") var boot: Bool = false - @Option(name: [.customShort("n")], help: "Number of lines to show from the end of the logs. If not provided this will print all of the logs") + @Flag(name: .shortAndLong, help: "Follow log output") + var follow: Bool = false + + @Option(name: .short, help: "Number of lines to show from the end of the logs. If not provided this will print all of the logs") var numLines: Int? - @Argument(help: "Container to fetch logs for") - var container: String + @OptionGroup + var global: Flags.Global + + @Argument(help: "Container ID") + var containerId: String public func run() async throws { do { - let container = try await ClientContainer.get(id: container) + let container = try await ClientContainer.get(id: containerId) let fhs = try await container.logs() let fileHandle = boot ? fhs[1] : fhs[0] @@ -59,7 +59,7 @@ extension Application { } catch { throw ContainerizationError( .invalidArgument, - message: "failed to fetch container logs for \(container): \(error)" + message: "failed to fetch container logs for \(containerId): \(error)" ) } } diff --git a/Sources/ContainerCommands/RunCommand.swift b/Sources/ContainerCommands/Container/ContainerRun.swift similarity index 97% rename from Sources/ContainerCommands/RunCommand.swift rename to Sources/ContainerCommands/Container/ContainerRun.swift index 6d5d6503..aadaa3da 100644 --- a/Sources/ContainerCommands/RunCommand.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -26,30 +26,30 @@ import NIOPosix import TerminalProgress extension Application { - public struct ContainerRunCommand: AsyncParsableCommand { + public struct ContainerRun: AsyncParsableCommand { public init() {} public static let configuration = CommandConfiguration( commandName: "run", abstract: "Run a container") - @OptionGroup + @OptionGroup(title: "Process options") var processFlags: Flags.Process - @OptionGroup + @OptionGroup(title: "Resource options") var resourceFlags: Flags.Resource - @OptionGroup + @OptionGroup(title: "Management options") var managementFlags: Flags.Management - @OptionGroup + @OptionGroup(title: "Registry options") var registryFlags: Flags.Registry + @OptionGroup(title: "Progress options") + var progressFlags: Flags.Progress + @OptionGroup var global: Flags.Global - @OptionGroup - var progressFlags: Flags.Progress - @Argument(help: "Image name") var image: String diff --git a/Sources/ContainerCommands/Container/ContainerStart.swift b/Sources/ContainerCommands/Container/ContainerStart.swift index 7369c080..cffedc67 100644 --- a/Sources/ContainerCommands/Container/ContainerStart.swift +++ b/Sources/ContainerCommands/Container/ContainerStart.swift @@ -31,14 +31,14 @@ extension Application { @Flag(name: .shortAndLong, help: "Attach STDOUT/STDERR") var attach = false - @Flag(name: .shortAndLong, help: "Attach container's STDIN") + @Flag(name: .shortAndLong, help: "Attach STDIN") var interactive = false @OptionGroup var global: Flags.Global - @Argument(help: "Container's ID") - var containerID: String + @Argument(help: "Container ID") + var containerId: String public func run() async throws { var exitCode: Int32 = 127 @@ -52,7 +52,7 @@ extension Application { } progress.start() - let container = try await ClientContainer.get(id: containerID) + let container = try await ClientContainer.get(id: containerId) do { let detach = !self.attach && !self.interactive let io = try ProcessIO.create( @@ -70,7 +70,7 @@ extension Application { if detach { try await process.start() try io.closeAfterStart() - print(self.containerID) + print(self.containerId) return } diff --git a/Sources/ContainerCommands/Container/ContainerStop.swift b/Sources/ContainerCommands/Container/ContainerStop.swift index 7ffa2866..d1eed1a1 100644 --- a/Sources/ContainerCommands/Container/ContainerStop.swift +++ b/Sources/ContainerCommands/Container/ContainerStop.swift @@ -31,30 +31,30 @@ extension Application { @Flag(name: .shortAndLong, help: "Stop all running containers") var all = false - @Option(name: .shortAndLong, help: "Signal to send the container(s)") + @Option(name: .shortAndLong, help: "Signal to send the containers") var signal: String = "SIGTERM" - @Option(name: .shortAndLong, help: "Seconds to wait before killing the container(s)") + @Option(name: .shortAndLong, help: "Seconds to wait before killing the containers") var time: Int32 = 5 - @Argument - var containerIDs: [String] = [] - @OptionGroup var global: Flags.Global + @Argument(help: "Container IDs") + var containerIds: [String] = [] + public func validate() throws { - if containerIDs.count == 0 && !all { + if containerIds.count == 0 && !all { throw ContainerizationError(.invalidArgument, message: "no containers specified and --all not supplied") } - if containerIDs.count > 0 && all { + if containerIds.count > 0 && all { throw ContainerizationError( .invalidArgument, message: "explicitly supplied container IDs conflicts with the --all flag") } } public mutating func run() async throws { - let set = Set(containerIDs) + let set = Set(containerIds) var containers = [ClientContainer]() if self.all { containers = try await ClientContainer.list() diff --git a/Sources/ContainerCommands/Container/ContainersCommand.swift b/Sources/ContainerCommands/Container/ContainersCommand.swift deleted file mode 100644 index 05abcd06..00000000 --- a/Sources/ContainerCommands/Container/ContainersCommand.swift +++ /dev/null @@ -1,40 +0,0 @@ -//===----------------------------------------------------------------------===// -// 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 - -extension Application { - public struct ContainersCommand: AsyncParsableCommand { - public init() {} - - public static let configuration = CommandConfiguration( - commandName: "containers", - abstract: "Manage containers", - subcommands: [ - ContainerCreate.self, - ContainerDelete.self, - ContainerExec.self, - ContainerInspect.self, - ContainerKill.self, - ContainerList.self, - ContainerLogs.self, - ContainerStart.self, - ContainerStop.self, - ], - aliases: ["container", "c"] - ) - } -} diff --git a/docs/command-reference.md b/docs/command-reference.md index 751558d5..0631c215 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -16,42 +16,47 @@ container run [OPTIONS] IMAGE [COMMAND] [ARG...] **Options** -* **Process and resources** - * `-w, --cwd, --workdir `: Current working directory for the container - * `-e, --env `: Set environment variables - * `--env-file `: Read in a file of environment variables - * `--uid `: Set the uid for the process - * `--gid `: Set the gid for the process - * `-i, --interactive`: Keep Stdin open even if not attached - * `-t, --tty`: Open a tty with the process - * `-u, --user `: Set the user for the process +* **Process options** + * `-e, --env `: Set environment variables (format: key=value) + * `--env-file `: Read in a file of environment variables (key=value format, ignores # comments and blank lines) + * `--gid `: Set the group ID for the process + * `-i, --interactive`: Keep the standard input open even if not attached + * `-t, --tty`: Open a TTY with the process + * `-u, --user `: Set the user for the process (format: name|uid[:gid]) + * `--uid `: Set the user ID for the process + * `-w, --workdir, --cwd `: Set the initial working directory inside the container +* **Resource options** * `-c, --cpus `: Number of CPUs to allocate to the container - * `-m, --memory `: 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) -* **Container management** - * `-d, --detach`: Run the container and detach from the process - * `--entrypoint `: Override the entrypoint of the image - * `--mount `: Add a mount to the container (type=<>,source=<>,target=<>,readonly) - * `-p, --publish `: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol]) - * `--publish-socket `: Publish a socket from container to host (format: host_path:container_path) - * `--tmpfs `: Add a tmpfs mount to the container at the given path - * `--name `: Assign a name to the container. If excluded will be a generated UUID - * `--remove, --rm`: Remove the container after it stops - * `--os `: Set OS if image can target multiple operating systems (default: linux) + * `-m, --memory `: Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix +* **Management options** * `-a, --arch `: Set arch if image can target multiple architectures (default: arm64) - * `-v, --volume `: Bind mount a volume into the container - * `-k, --kernel `: Set a custom kernel path - * `--network `: Attach the container to a network * `--cidfile `: Write the container ID to the path provided - * `--no-dns`: Do not configure DNS in the container - * `--dns `: DNS nameserver IP address - * `--dns-domain `: Default DNS domain - * `--dns-search `: DNS search domains - * `--dns-option `: DNS options + * `-d, --detach`: Run the container and detach from the process + * `--dns `: DNS nameserver IP address + * `--dns-domain `: Default DNS domain + * `--dns-option