From 940668af3998058cd655757c73945ee2a28bd6ab Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Wed, 7 May 2025 12:46:15 +0100 Subject: [PATCH 1/9] containertool: Arrange options into related groups --- Sources/containertool/containertool.swift | 152 +++++++++++++--------- 1 file changed, 91 insertions(+), 61 deletions(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index ef8c543..492e9b5 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -28,85 +28,111 @@ 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: "Default registry for image references which do not 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: "Destination image reference") + var repository: String? + + @Option(help: "Destination image tag") + var tag: String? + + @Option(help: "Base image reference") + var from: String? + } + + @OptionGroup(title: "Source and destination repository options") + var repositoryOptions: RepositoryOptions + + /// 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: "Default username, used if there are no matching entries in .netrc") - private var defaultUsername: String? + @Option(help: "Operating system") + var os: String? + } + + @OptionGroup(title: "Image configuration options") + var imageConfigurationOptions: ImageConfigurationOptions - @Option( - help: ArgumentHelp( - "[DEPRECATED] Default password, used if there are no matching entries in .netrc. Use --default-password instead.", - visibility: .private + /// 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 + ) ) - ) - private var password: String? + var username: String? - @Option(help: "Default password, used if there are no matching entries in .netrc") - private var defaultPassword: String? + @Option(help: "Default username, used if there are no matching entries in .netrc") + var defaultUsername: String? - @Flag(name: .shortAndLong, help: "Verbose output") - private var verbose: Bool = false + @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: "Connect to the container registry using plaintext HTTP") - private var allowInsecureHttp: AllowHTTP? + @Option(help: "Default password, used if there are no matching entries in .netrc") + var defaultPassword: String? - @Option(help: "CPU architecture") - private var architecture: String? + @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file") + var netrc: Bool = true - @Option(help: "Base image reference") - private var from: String? + @Option(help: "Specify the netrc file path") + var netrcFile: String? - @Option(help: "Operating system") - private var os: String? + @Option(help: "Connect to the registry using plaintext HTTP") + var allowInsecureHttp: AllowHTTP? + } - @Option(help: "Tag for this manifest") - private var tag: String? + @OptionGroup(title: "Authentication options") + var authenticationOptions: AuthenticationOptions - @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file") - private var netrc: Bool = true + // General options - @Option(help: "Specify the netrc file path") - private var netrcFile: String? + @Flag(name: .shortAndLong, help: "Verbose output") + private var verbose: Bool = false mutating func validate() throws { - if username != nil { - guard defaultUsername == nil else { + if authenticationOptions.username != nil { + guard authenticationOptions.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 + authenticationOptions.defaultUsername = authenticationOptions.username } - if password != nil { - guard defaultPassword == nil else { + if authenticationOptions.password != nil { + guard authenticationOptions.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 + authenticationOptions.defaultPassword = authenticationOptions.password } } @@ -115,17 +141,17 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti 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 +159,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 +168,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 +194,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 +203,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 +219,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 ) From b2d3a6c664c6c52360434d818323862964ab203f Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Wed, 7 May 2025 14:00:26 +0100 Subject: [PATCH 2/9] containertool: Move authentication validation into the authentication option struct --- Sources/containertool/containertool.swift | 51 ++++++++++++----------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 492e9b5..49a898c 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -102,6 +102,33 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Option(help: "Connect to the registry using plaintext HTTP") var allowInsecureHttp: AllowHTTP? + + 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." + ) + } + + 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." + ) + } + + log("Deprecation warning: --password is deprecated, please use --default-password instead.") + defaultPassword = password + } + } } @OptionGroup(title: "Authentication options") @@ -112,30 +139,6 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Flag(name: .shortAndLong, help: "Verbose output") private var verbose: Bool = false - mutating func validate() throws { - if authenticationOptions.username != nil { - guard authenticationOptions.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.") - authenticationOptions.defaultUsername = authenticationOptions.username - } - - if authenticationOptions.password != nil { - guard authenticationOptions.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.") - authenticationOptions.defaultPassword = authenticationOptions.password - } - } - func run() async throws { // MARK: Apply defaults for unspecified configuration flags From c462f2a83f80c6882fdfde2a78c500fc5c98aed9 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Wed, 7 May 2025 15:42:02 +0100 Subject: [PATCH 3/9] docs: Rearrange options on build-container-image manual page to match --help --- .../build-container-image.md | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) 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..f311d2c 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 `: + Destination image tag. + + 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`) From cede2041188e492cb7441d3bf6ad10ae4329baa3 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Thu, 8 May 2025 16:01:13 +0100 Subject: [PATCH 4/9] Update Sources/containertool/containertool.swift Co-authored-by: Joseph Heck --- Sources/containertool/containertool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 49a898c..0303a7e 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -33,7 +33,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti /// Options controlling the locations of the source and destination images struct RepositoryOptions: ParsableArguments { - @Option(help: "Default registry for image references which do not specify one") + @Option(help: "The default container registry to use when the image reference doesn't specify one") var defaultRegistry: String? @Option(help: "Destination image reference") From de4dc5ba626d9b320afebc6a05eaab78558a5b3e Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 9 May 2025 15:22:44 +0100 Subject: [PATCH 5/9] Update Sources/swift-container-plugin/Documentation.docc/build-container-image.md Co-authored-by: Joseph Heck --- .../Documentation.docc/build-container-image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f311d2c..4ec55ad 100644 --- a/Sources/swift-container-plugin/Documentation.docc/build-container-image.md +++ b/Sources/swift-container-plugin/Documentation.docc/build-container-image.md @@ -31,7 +31,7 @@ Wrap a binary in a container image and publish it. The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable. - term `--tag `: - Destination image tag. + The tag to apply to the destination image. The `latest` tag is automatically updated to refer to the published image. From d862b244b1452fdb9a56e561d7ba460bc4315f74 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 9 May 2025 15:23:49 +0100 Subject: [PATCH 6/9] Update Sources/containertool/containertool.swift Co-authored-by: Joseph Heck --- Sources/containertool/containertool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 0303a7e..051f2c9 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -39,7 +39,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Option(help: "Destination image reference") var repository: String? - @Option(help: "Destination image tag") + @Option(help: "The tag for the generated container image") var tag: String? @Option(help: "Base image reference") From 3b8aa46f6fc87c7749e940b9d15d224abfafd72a Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 9 May 2025 15:25:41 +0100 Subject: [PATCH 7/9] Update Sources/containertool/containertool.swift Co-authored-by: Joseph Heck --- Sources/containertool/containertool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 051f2c9..95484f9 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -91,7 +91,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti ) var password: String? - @Option(help: "Default password, used if there are no matching entries in .netrc") + @Option(help: "The default password to use if the tool can't find a matching entry in .netrc") var defaultPassword: String? @Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file") From 350d42513fbd62a0af611565c5a8a2de47588cf3 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 9 May 2025 15:26:39 +0100 Subject: [PATCH 8/9] Update Sources/containertool/containertool.swift Co-authored-by: Joseph Heck --- Sources/containertool/containertool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index 95484f9..af17ae9 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -42,7 +42,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Option(help: "The tag for the generated container image") var tag: String? - @Option(help: "Base image reference") + @Option(help: "The base container image name and optional tag") var from: String? } From c1c13bf20bb8bd26af860eb7275b64c709dfe2b2 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 9 May 2025 15:28:13 +0100 Subject: [PATCH 9/9] Update Sources/containertool/containertool.swift Co-authored-by: Joseph Heck --- Sources/containertool/containertool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index af17ae9..2afca70 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -36,7 +36,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti @Option(help: "The default container registry to use when the image reference doesn't specify one") var defaultRegistry: String? - @Option(help: "Destination image reference") + @Option(help: "The name and optional tag for the generated container image") var repository: String? @Option(help: "The tag for the generated container image")