diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index ef8c543..2afca70 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -28,104 +28,133 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti abstract: "Build and publish a container image" ) - @Option(help: "Default registry for references which do not specify a registry") - private var defaultRegistry: String? - - @Option(help: "Repository path") - private var repository: String? - @Argument(help: "Executable to package") private var executable: String - @Option(help: "Resource bundle directory") - private var resources: [String] = [] + /// Options controlling the locations of the source and destination images + struct RepositoryOptions: ParsableArguments { + @Option(help: "The default container registry to use when the image reference doesn't specify one") + var defaultRegistry: String? - @Option( - help: ArgumentHelp( - "[DEPRECATED] Default username, used if there are no matching entries in .netrc. Use --default-username instead.", - visibility: .private - ) - ) - private var username: String? + @Option(help: "The name and optional tag for the generated container image") + var repository: String? - @Option(help: "Default username, used if there are no matching entries in .netrc") - private var defaultUsername: String? + @Option(help: "The tag for the generated container image") + var tag: String? - @Option( - help: ArgumentHelp( - "[DEPRECATED] Default password, used if there are no matching entries in .netrc. Use --default-password instead.", - visibility: .private - ) - ) - private var password: String? + @Option(help: "The base container image name and optional tag") + var from: String? + } - @Option(help: "Default password, used if there are no matching entries in .netrc") - private var defaultPassword: String? + @OptionGroup(title: "Source and destination repository options") + var repositoryOptions: RepositoryOptions - @Flag(name: .shortAndLong, help: "Verbose output") - private var verbose: Bool = false + /// Options controlling how the destination image is built + struct ImageBuildOptions: ParsableArguments { + @Option(help: "Directory of resources to include in the image") + var resources: [String] = [] + } + + @OptionGroup(title: "Image build options") + var imageBuildOptions: ImageBuildOptions + + // Options controlling the destination image's runtime configuration + struct ImageConfigurationOptions: ParsableArguments { + @Option(help: "CPU architecture") + var architecture: String? + + @Option(help: "Operating system") + var os: String? + } + + @OptionGroup(title: "Image configuration options") + var imageConfigurationOptions: ImageConfigurationOptions - @Option(help: "Connect to the container registry using plaintext HTTP") - private var allowInsecureHttp: AllowHTTP? + /// Options controlling how containertool authenticates to registries + struct AuthenticationOptions: ParsableArguments { + @Option( + help: ArgumentHelp( + "[DEPRECATED] Default username, used if there are no matching entries in .netrc. Use --default-username instead.", + visibility: .private + ) + ) + var username: String? - @Option(help: "CPU architecture") - private var architecture: String? + @Option(help: "Default username, used if there are no matching entries in .netrc") + var defaultUsername: String? + + @Option( + help: ArgumentHelp( + "[DEPRECATED] Default password, used if there are no matching entries in .netrc. Use --default-password instead.", + visibility: .private + ) + ) + var password: String? - @Option(help: "Base image reference") - private var from: String? + @Option(help: "The default password to use if the tool can't find a matching entry in .netrc") + var defaultPassword: String? - @Option(help: "Operating system") - private var os: String? + @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file") + var netrc: Bool = true - @Option(help: "Tag for this manifest") - private var tag: String? + @Option(help: "Specify the netrc file path") + var netrcFile: String? - @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file") - private var netrc: Bool = true + @Option(help: "Connect to the registry using plaintext HTTP") + var allowInsecureHttp: AllowHTTP? - @Option(help: "Specify the netrc file path") - private var netrcFile: String? + mutating func validate() throws { + // The `--username` and `--password` options present v1.0 were deprecated and replaced by more descriptive + // `--default-username` and `--default-password`. The old names are still accepted, but specifying both the old + // and the new names at the same time is ambiguous and causes an error. + if username != nil { + guard defaultUsername == nil else { + throw ValidationError( + "--default-username and --username cannot be specified together. --username is deprecated, please use --default-username instead." + ) + } - mutating func validate() throws { - if username != nil { - guard defaultUsername == nil else { - throw ValidationError( - "--default-username and --username cannot be specified together. Please use --default-username only." - ) + log("Deprecation warning: --username is deprecated, please use --default-username instead.") + defaultUsername = username } - log("Deprecation warning: --username is deprecated, please use --default-username instead.") - defaultUsername = username - } + if password != nil { + guard defaultPassword == nil else { + throw ValidationError( + "--default-password and --password cannot be specified together. --password is deprecated, please use --default-password instead." + ) + } - if password != nil { - guard defaultPassword == nil else { - throw ValidationError( - "--default-password and --password cannot be specified together. Please use --default-password only." - ) + log("Deprecation warning: --password is deprecated, please use --default-password instead.") + defaultPassword = password } - - log("Deprecation warning: --password is deprecated, please use --default-password instead.") - defaultPassword = password } } + @OptionGroup(title: "Authentication options") + var authenticationOptions: AuthenticationOptions + + // General options + + @Flag(name: .shortAndLong, help: "Verbose output") + private var verbose: Bool = false + func run() async throws { // MARK: Apply defaults for unspecified configuration flags let env = ProcessInfo.processInfo.environment - let defaultRegistry = defaultRegistry ?? env["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io" - guard let repository = repository ?? env["CONTAINERTOOL_REPOSITORY"] else { + let defaultRegistry = repositoryOptions.defaultRegistry ?? env["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io" + guard let repository = repositoryOptions.repository ?? env["CONTAINERTOOL_REPOSITORY"] else { throw ValidationError( "Please specify the destination repository using --repository or CONTAINERTOOL_REPOSITORY" ) } - let username = defaultUsername ?? env["CONTAINERTOOL_DEFAULT_USERNAME"] - let password = defaultPassword ?? env["CONTAINERTOOL_DEFAULT_PASSWORD"] - let from = from ?? env["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim" - let os = os ?? env["CONTAINERTOOL_OS"] ?? "linux" + let username = authenticationOptions.defaultUsername ?? env["CONTAINERTOOL_DEFAULT_USERNAME"] + let password = authenticationOptions.defaultPassword ?? env["CONTAINERTOOL_DEFAULT_PASSWORD"] + let from = repositoryOptions.from ?? env["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim" + let os = imageConfigurationOptions.os ?? env["CONTAINERTOOL_OS"] ?? "linux" // Try to detect the architecture of the application executable so a suitable base image can be selected. // This reduces the risk of accidentally creating an image which stacks an aarch64 executable on top of an x86_64 base image. @@ -133,7 +162,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti let elfheader = try ELF.read(at: executableURL) let architecture = - architecture + imageConfigurationOptions.architecture ?? env["CONTAINERTOOL_ARCHITECTURE"] ?? elfheader?.ISA.containerArchitecture ?? "amd64" @@ -142,10 +171,12 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti // MARK: Load netrc let authProvider: AuthorizationProvider? - if !netrc { + if !authenticationOptions.netrc { authProvider = nil - } else if let netrcFile { - guard FileManager.default.fileExists(atPath: netrcFile) else { throw "\(netrcFile) not found" } + } else if let netrcFile = authenticationOptions.netrcFile { + guard FileManager.default.fileExists(atPath: netrcFile) else { + throw "\(netrcFile) not found" + } let customNetrc = URL(fileURLWithPath: netrcFile) authProvider = try NetrcAuthorizationProvider(customNetrc) } else { @@ -166,7 +197,8 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti } else { source = try await RegistryClient( registry: baseImage.registry, - insecure: allowInsecureHttp == .source || allowInsecureHttp == .both, + insecure: authenticationOptions.allowInsecureHttp == .source + || authenticationOptions.allowInsecureHttp == .both, auth: .init(username: username, password: password, auth: authProvider) ) if verbose { log("Connected to source registry: \(baseImage.registry)") } @@ -174,7 +206,8 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti let destination = try await RegistryClient( registry: destinationImage.registry, - insecure: allowInsecureHttp == .destination || allowInsecureHttp == .both, + insecure: authenticationOptions.allowInsecureHttp == .destination + || authenticationOptions.allowInsecureHttp == .both, auth: .init(username: username, password: password, auth: authProvider) ) @@ -189,8 +222,8 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti source: source, architecture: architecture, os: os, - resources: resources, - tag: tag, + resources: imageBuildOptions.resources, + tag: repositoryOptions.tag, verbose: verbose, executableURL: executableURL ) diff --git a/Sources/swift-container-plugin/Documentation.docc/build-container-image.md b/Sources/swift-container-plugin/Documentation.docc/build-container-image.md index a5fb5c3..4ec55ad 100644 --- a/Sources/swift-container-plugin/Documentation.docc/build-container-image.md +++ b/Sources/swift-container-plugin/Documentation.docc/build-container-image.md @@ -8,7 +8,7 @@ Wrap a binary in a container image and publish it. ### Usage -`swift package build-container-image [] --repository ` +`swift package build-container-image []` ### Options @@ -17,15 +17,28 @@ Wrap a binary in a container image and publish it. If `Package.swift` defines only one product, it will be selected by default. +### Source and destination repository options + - term `--default-registry `: The default registry hostname. (default: `docker.io`) If the repository path does not contain a registry hostname, the default registry will be prepended to it. - term `--repository `: - The repository path. + Destination image repository. - If the path does not begin with a registry hostname, the default registry will be prepended to the path. + If the repository path does not begin with a registry hostname, the default registry will be prepended to the path. + The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable. + +- term `--tag `: + The tag to apply to the destination image. + + The `latest` tag is automatically updated to refer to the published image. + +- term `--from `: + Base image reference. (default: `swift:slim`) + +### Image build options - term `--resources `: Add the file or directory at `resources` to the image. @@ -33,6 +46,18 @@ Wrap a binary in a container image and publish it. If the `product` being packaged has a [resource bundle](https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package) it will be added to the image automatically. +### Image configuration options + +- term `--architecture `: + CPU architecture required to run the image. + + If the base image is `scratch`, the final image will have no base layer and will consist only of the application layer and resource bundle layer, if the product has a resource bundle. + +- term `--os `: + Operating system required to run the image. (default: `linux`) + +### Authentication options + - term `--default-username `: Default username to use when logging into the registry. @@ -45,34 +70,20 @@ Wrap a binary in a container image and publish it. This password is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. The same password is used for the source and destination registries. -- term `-v, --verbose`: - Verbose output. - -- term `--allow-insecure-http `: - Connect to the container registry using plaintext HTTP. (values: `source`, `destination`, `both`) - -- term `--architecture `: - CPU architecture to record in the image. - -- term `--from `: - Base image reference. (default: `swift:slim`) - - If the base image is `scratch`, the final image will have no base layer and will consist only of the application layer and resource bundle layer, if the product has a resource bundle. - -- term `--os `: - Operating system to record in the image. (default: `linux`) - -- term `--tag `: - Tag for this manifest. - - The `latest` tag is automatically updated to refer to the published image. - - term `--enable-netrc/--disable-netrc`: Load credentials from a netrc file (default: `--enable-netrc`) - term `--netrc-file `: The path to the `.netrc` file. +- term `--allow-insecure-http `: + Connect to the container registry using plaintext HTTP. (values: `source`, `destination`, `both`) + +### Options + +- term `-v, --verbose`: + Verbose output. + - term `-h, --help`: Show help information. @@ -83,14 +94,15 @@ Wrap a binary in a container image and publish it. (default: `docker.io`) - term `CONTAINERTOOL_REPOSITORY`: - The repository path. + The destination image repository. If the path does not begin with a registry hostname, the default registry will be prepended to the path. + The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable. - term `CONTAINERTOOL_BASE_IMAGE`: Base image on which to layer the application. (default: `swift:slim`) - term `CONTAINERTOOL_OS`: - Operating system to encode in the container image. + Operating system. (default: `Linux`)