Skip to content

Commit 3030089

Browse files
euanhheckj
andauthored
containertool: Arrange options into related groups (#128)
Motivation ---------- Arranging related options together in groups makes it easier for users to find relevant options. This will be even more beneficial as more options are added (e.g. #91). Modifications ------------- * Arrange `containertool`'s command line options into related groups with headings. * Update `build-container-image` manual page to match the new groupings. Result ------ * Users will be able to find help for specific options more easily. * Grouping options together may also help them to discover new options related the tasks they are trying to achieve. Test Plan --------- * No functional change. * All existing tests continue to pass. --------- Co-authored-by: Joseph Heck <[email protected]>
1 parent 5ea0c5d commit 3030089

File tree

2 files changed

+145
-100
lines changed

2 files changed

+145
-100
lines changed

Sources/containertool/containertool.swift

Lines changed: 106 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -28,112 +28,141 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
2828
abstract: "Build and publish a container image"
2929
)
3030

31-
@Option(help: "Default registry for references which do not specify a registry")
32-
private var defaultRegistry: String?
33-
34-
@Option(help: "Repository path")
35-
private var repository: String?
36-
3731
@Argument(help: "Executable to package")
3832
private var executable: String
3933

40-
@Option(help: "Resource bundle directory")
41-
private var resources: [String] = []
34+
/// Options controlling the locations of the source and destination images
35+
struct RepositoryOptions: ParsableArguments {
36+
@Option(help: "The default container registry to use when the image reference doesn't specify one")
37+
var defaultRegistry: String?
4238

43-
@Option(
44-
help: ArgumentHelp(
45-
"[DEPRECATED] Default username, used if there are no matching entries in .netrc. Use --default-username instead.",
46-
visibility: .private
47-
)
48-
)
49-
private var username: String?
39+
@Option(help: "The name and optional tag for the generated container image")
40+
var repository: String?
5041

51-
@Option(help: "Default username, used if there are no matching entries in .netrc")
52-
private var defaultUsername: String?
42+
@Option(help: "The tag for the generated container image")
43+
var tag: String?
5344

54-
@Option(
55-
help: ArgumentHelp(
56-
"[DEPRECATED] Default password, used if there are no matching entries in .netrc. Use --default-password instead.",
57-
visibility: .private
58-
)
59-
)
60-
private var password: String?
45+
@Option(help: "The base container image name and optional tag")
46+
var from: String?
47+
}
6148

62-
@Option(help: "Default password, used if there are no matching entries in .netrc")
63-
private var defaultPassword: String?
49+
@OptionGroup(title: "Source and destination repository options")
50+
var repositoryOptions: RepositoryOptions
6451

65-
@Flag(name: .shortAndLong, help: "Verbose output")
66-
private var verbose: Bool = false
52+
/// Options controlling how the destination image is built
53+
struct ImageBuildOptions: ParsableArguments {
54+
@Option(help: "Directory of resources to include in the image")
55+
var resources: [String] = []
56+
}
57+
58+
@OptionGroup(title: "Image build options")
59+
var imageBuildOptions: ImageBuildOptions
60+
61+
// Options controlling the destination image's runtime configuration
62+
struct ImageConfigurationOptions: ParsableArguments {
63+
@Option(help: "CPU architecture")
64+
var architecture: String?
65+
66+
@Option(help: "Operating system")
67+
var os: String?
68+
}
69+
70+
@OptionGroup(title: "Image configuration options")
71+
var imageConfigurationOptions: ImageConfigurationOptions
6772

68-
@Option(help: "Connect to the container registry using plaintext HTTP")
69-
private var allowInsecureHttp: AllowHTTP?
73+
/// Options controlling how containertool authenticates to registries
74+
struct AuthenticationOptions: ParsableArguments {
75+
@Option(
76+
help: ArgumentHelp(
77+
"[DEPRECATED] Default username, used if there are no matching entries in .netrc. Use --default-username instead.",
78+
visibility: .private
79+
)
80+
)
81+
var username: String?
7082

71-
@Option(help: "CPU architecture")
72-
private var architecture: String?
83+
@Option(help: "Default username, used if there are no matching entries in .netrc")
84+
var defaultUsername: String?
85+
86+
@Option(
87+
help: ArgumentHelp(
88+
"[DEPRECATED] Default password, used if there are no matching entries in .netrc. Use --default-password instead.",
89+
visibility: .private
90+
)
91+
)
92+
var password: String?
7393

74-
@Option(help: "Base image reference")
75-
private var from: String?
94+
@Option(help: "The default password to use if the tool can't find a matching entry in .netrc")
95+
var defaultPassword: String?
7696

77-
@Option(help: "Operating system")
78-
private var os: String?
97+
@Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file")
98+
var netrc: Bool = true
7999

80-
@Option(help: "Tag for this manifest")
81-
private var tag: String?
100+
@Option(help: "Specify the netrc file path")
101+
var netrcFile: String?
82102

83-
@Flag(inversion: .prefixedEnableDisable, exclusivity: .exclusive, help: "Load credentials from a netrc file")
84-
private var netrc: Bool = true
103+
@Option(help: "Connect to the registry using plaintext HTTP")
104+
var allowInsecureHttp: AllowHTTP?
85105

86-
@Option(help: "Specify the netrc file path")
87-
private var netrcFile: String?
106+
mutating func validate() throws {
107+
// The `--username` and `--password` options present v1.0 were deprecated and replaced by more descriptive
108+
// `--default-username` and `--default-password`. The old names are still accepted, but specifying both the old
109+
// and the new names at the same time is ambiguous and causes an error.
110+
if username != nil {
111+
guard defaultUsername == nil else {
112+
throw ValidationError(
113+
"--default-username and --username cannot be specified together. --username is deprecated, please use --default-username instead."
114+
)
115+
}
88116

89-
mutating func validate() throws {
90-
if username != nil {
91-
guard defaultUsername == nil else {
92-
throw ValidationError(
93-
"--default-username and --username cannot be specified together. Please use --default-username only."
94-
)
117+
log("Deprecation warning: --username is deprecated, please use --default-username instead.")
118+
defaultUsername = username
95119
}
96120

97-
log("Deprecation warning: --username is deprecated, please use --default-username instead.")
98-
defaultUsername = username
99-
}
121+
if password != nil {
122+
guard defaultPassword == nil else {
123+
throw ValidationError(
124+
"--default-password and --password cannot be specified together. --password is deprecated, please use --default-password instead."
125+
)
126+
}
100127

101-
if password != nil {
102-
guard defaultPassword == nil else {
103-
throw ValidationError(
104-
"--default-password and --password cannot be specified together. Please use --default-password only."
105-
)
128+
log("Deprecation warning: --password is deprecated, please use --default-password instead.")
129+
defaultPassword = password
106130
}
107-
108-
log("Deprecation warning: --password is deprecated, please use --default-password instead.")
109-
defaultPassword = password
110131
}
111132
}
112133

134+
@OptionGroup(title: "Authentication options")
135+
var authenticationOptions: AuthenticationOptions
136+
137+
// General options
138+
139+
@Flag(name: .shortAndLong, help: "Verbose output")
140+
private var verbose: Bool = false
141+
113142
func run() async throws {
114143
// MARK: Apply defaults for unspecified configuration flags
115144

116145
let env = ProcessInfo.processInfo.environment
117146

118-
let defaultRegistry = defaultRegistry ?? env["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io"
119-
guard let repository = repository ?? env["CONTAINERTOOL_REPOSITORY"] else {
147+
let defaultRegistry = repositoryOptions.defaultRegistry ?? env["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io"
148+
guard let repository = repositoryOptions.repository ?? env["CONTAINERTOOL_REPOSITORY"] else {
120149
throw ValidationError(
121150
"Please specify the destination repository using --repository or CONTAINERTOOL_REPOSITORY"
122151
)
123152
}
124153

125-
let username = defaultUsername ?? env["CONTAINERTOOL_DEFAULT_USERNAME"]
126-
let password = defaultPassword ?? env["CONTAINERTOOL_DEFAULT_PASSWORD"]
127-
let from = from ?? env["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim"
128-
let os = os ?? env["CONTAINERTOOL_OS"] ?? "linux"
154+
let username = authenticationOptions.defaultUsername ?? env["CONTAINERTOOL_DEFAULT_USERNAME"]
155+
let password = authenticationOptions.defaultPassword ?? env["CONTAINERTOOL_DEFAULT_PASSWORD"]
156+
let from = repositoryOptions.from ?? env["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim"
157+
let os = imageConfigurationOptions.os ?? env["CONTAINERTOOL_OS"] ?? "linux"
129158

130159
// Try to detect the architecture of the application executable so a suitable base image can be selected.
131160
// This reduces the risk of accidentally creating an image which stacks an aarch64 executable on top of an x86_64 base image.
132161
let executableURL = URL(fileURLWithPath: executable)
133162
let elfheader = try ELF.read(at: executableURL)
134163

135164
let architecture =
136-
architecture
165+
imageConfigurationOptions.architecture
137166
?? env["CONTAINERTOOL_ARCHITECTURE"]
138167
?? elfheader?.ISA.containerArchitecture
139168
?? "amd64"
@@ -142,10 +171,12 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
142171
// MARK: Load netrc
143172

144173
let authProvider: AuthorizationProvider?
145-
if !netrc {
174+
if !authenticationOptions.netrc {
146175
authProvider = nil
147-
} else if let netrcFile {
148-
guard FileManager.default.fileExists(atPath: netrcFile) else { throw "\(netrcFile) not found" }
176+
} else if let netrcFile = authenticationOptions.netrcFile {
177+
guard FileManager.default.fileExists(atPath: netrcFile) else {
178+
throw "\(netrcFile) not found"
179+
}
149180
let customNetrc = URL(fileURLWithPath: netrcFile)
150181
authProvider = try NetrcAuthorizationProvider(customNetrc)
151182
} else {
@@ -166,15 +197,17 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
166197
} else {
167198
source = try await RegistryClient(
168199
registry: baseImage.registry,
169-
insecure: allowInsecureHttp == .source || allowInsecureHttp == .both,
200+
insecure: authenticationOptions.allowInsecureHttp == .source
201+
|| authenticationOptions.allowInsecureHttp == .both,
170202
auth: .init(username: username, password: password, auth: authProvider)
171203
)
172204
if verbose { log("Connected to source registry: \(baseImage.registry)") }
173205
}
174206

175207
let destination = try await RegistryClient(
176208
registry: destinationImage.registry,
177-
insecure: allowInsecureHttp == .destination || allowInsecureHttp == .both,
209+
insecure: authenticationOptions.allowInsecureHttp == .destination
210+
|| authenticationOptions.allowInsecureHttp == .both,
178211
auth: .init(username: username, password: password, auth: authProvider)
179212
)
180213

@@ -189,8 +222,8 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
189222
source: source,
190223
architecture: architecture,
191224
os: os,
192-
resources: resources,
193-
tag: tag,
225+
resources: imageBuildOptions.resources,
226+
tag: repositoryOptions.tag,
194227
verbose: verbose,
195228
executableURL: executableURL
196229
)

Sources/swift-container-plugin/Documentation.docc/build-container-image.md

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Wrap a binary in a container image and publish it.
88

99
### Usage
1010

11-
`swift package build-container-image [<options>] --repository <repository>`
11+
`swift package build-container-image [<options>]`
1212

1313
### Options
1414

@@ -17,22 +17,47 @@ Wrap a binary in a container image and publish it.
1717

1818
If `Package.swift` defines only one product, it will be selected by default.
1919

20+
### Source and destination repository options
21+
2022
- term `--default-registry <default-registry>`:
2123
The default registry hostname. (default: `docker.io`)
2224

2325
If the repository path does not contain a registry hostname, the default registry will be prepended to it.
2426

2527
- term `--repository <repository>`:
26-
The repository path.
28+
Destination image repository.
2729

28-
If the path does not begin with a registry hostname, the default registry will be prepended to the path.
30+
If the repository path does not begin with a registry hostname, the default registry will be prepended to the path.
31+
The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable.
32+
33+
- term `--tag <tag>`:
34+
The tag to apply to the destination image.
35+
36+
The `latest` tag is automatically updated to refer to the published image.
37+
38+
- term `--from <from>`:
39+
Base image reference. (default: `swift:slim`)
40+
41+
### Image build options
2942

3043
- term `--resources <resources>`:
3144
Add the file or directory at `resources` to the image.
3245
Directories are added recursively.
3346

3447
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.
3548

49+
### Image configuration options
50+
51+
- term `--architecture <architecture>`:
52+
CPU architecture required to run the image.
53+
54+
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.
55+
56+
- term `--os <os>`:
57+
Operating system required to run the image. (default: `linux`)
58+
59+
### Authentication options
60+
3661
- term `--default-username <username>`:
3762
Default username to use when logging into the registry.
3863

@@ -45,34 +70,20 @@ Wrap a binary in a container image and publish it.
4570
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.
4671
The same password is used for the source and destination registries.
4772

48-
- term `-v, --verbose`:
49-
Verbose output.
50-
51-
- term `--allow-insecure-http <allow-insecure-http>`:
52-
Connect to the container registry using plaintext HTTP. (values: `source`, `destination`, `both`)
53-
54-
- term `--architecture <architecture>`:
55-
CPU architecture to record in the image.
56-
57-
- term `--from <from>`:
58-
Base image reference. (default: `swift:slim`)
59-
60-
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.
61-
62-
- term `--os <os>`:
63-
Operating system to record in the image. (default: `linux`)
64-
65-
- term `--tag <tag>`:
66-
Tag for this manifest.
67-
68-
The `latest` tag is automatically updated to refer to the published image.
69-
7073
- term `--enable-netrc/--disable-netrc`:
7174
Load credentials from a netrc file (default: `--enable-netrc`)
7275

7376
- term `--netrc-file <netrc-file>`:
7477
The path to the `.netrc` file.
7578

79+
- term `--allow-insecure-http <allow-insecure-http>`:
80+
Connect to the container registry using plaintext HTTP. (values: `source`, `destination`, `both`)
81+
82+
### Options
83+
84+
- term `-v, --verbose`:
85+
Verbose output.
86+
7687
- term `-h, --help`:
7788
Show help information.
7889

@@ -83,9 +94,10 @@ Wrap a binary in a container image and publish it.
8394
(default: `docker.io`)
8495

8596
- term `CONTAINERTOOL_REPOSITORY`:
86-
The repository path.
97+
The destination image repository.
8798

8899
If the path does not begin with a registry hostname, the default registry will be prepended to the path.
100+
The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable.
89101

90102
- term `CONTAINERTOOL_BASE_IMAGE`:
91103
Base image on which to layer the application.
@@ -95,7 +107,7 @@ Wrap a binary in a container image and publish it.
95107
CPU architecture.
96108

97109
- term `CONTAINERTOOL_OS`:
98-
Operating system to encode in the container image.
110+
Operating system.
99111
(default: `Linux`)
100112

101113
- term `CONTAINERTOOL_DEFAULT_USERNAME`:

0 commit comments

Comments
 (0)