mirror of
https://github.com/apple/container.git
synced 2026-07-12 12:37:02 +00:00
add support for multiple --tag flags in build (#785)
- Closes #732. ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Adds support for specifying multiple `-t` / `--tag` flags with the `build` command. All specified tags will point to the same built image. Example: `container build -t myapp:latest -t myapp:v1.0.0 -t myapp:stable .` ## Testing - [x] Tested locally - [x] Added/updated tests - [x] Added/updated docs
This commit is contained in:
@@ -243,7 +243,7 @@ public struct Builder: Sendable {
|
||||
public let noCache: Bool
|
||||
public let platforms: [Platform]
|
||||
public let terminal: Terminal?
|
||||
public let tag: String
|
||||
public let tags: [String]
|
||||
public let target: String
|
||||
public let quiet: Bool
|
||||
public let exports: [BuildExport]
|
||||
@@ -260,7 +260,7 @@ public struct Builder: Sendable {
|
||||
noCache: Bool,
|
||||
platforms: [Platform],
|
||||
terminal: Terminal?,
|
||||
tag: String,
|
||||
tags: [String],
|
||||
target: String,
|
||||
quiet: Bool,
|
||||
exports: [BuildExport],
|
||||
@@ -276,7 +276,7 @@ public struct Builder: Sendable {
|
||||
self.noCache = noCache
|
||||
self.platforms = platforms
|
||||
self.terminal = terminal
|
||||
self.tag = tag
|
||||
self.tags = tags
|
||||
self.target = target
|
||||
self.quiet = quiet
|
||||
self.exports = exports
|
||||
@@ -312,9 +312,11 @@ extension CallOptions {
|
||||
("context", URL(filePath: config.contextDir).path(percentEncoded: false)),
|
||||
("dockerfile", config.dockerfile.base64EncodedString()),
|
||||
("progress", config.terminal != nil ? "tty" : "plain"),
|
||||
("tag", config.tag),
|
||||
("target", config.target),
|
||||
]
|
||||
for tag in config.tags {
|
||||
headers.append(("tag", tag))
|
||||
}
|
||||
for platform in config.platforms {
|
||||
headers.append(("platforms", platform.description))
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@ extension Application {
|
||||
var quiet: Bool = false
|
||||
|
||||
@Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name"))
|
||||
var targetImageName: String = UUID().uuidString.lowercased()
|
||||
var targetImageNames: [String] = {
|
||||
[UUID().uuidString.lowercased()]
|
||||
}()
|
||||
|
||||
@Option(name: .long, help: ArgumentHelp("Set the target build stage", valueName: "stage"))
|
||||
var target: String = ""
|
||||
@@ -195,11 +197,11 @@ extension Application {
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
|
||||
let imageName: String = try {
|
||||
let parsedReference = try Reference.parse(targetImageName)
|
||||
let imageNames: [String] = try targetImageNames.map { name in
|
||||
let parsedReference = try Reference.parse(name)
|
||||
parsedReference.normalize()
|
||||
return parsedReference.description
|
||||
}()
|
||||
}
|
||||
|
||||
var terminal: Terminal?
|
||||
switch self.progress {
|
||||
@@ -267,7 +269,7 @@ extension Application {
|
||||
noCache: noCache,
|
||||
platforms: [Platform](platforms),
|
||||
terminal: terminal,
|
||||
tag: imageName,
|
||||
tags: imageNames,
|
||||
target: target,
|
||||
quiet: quiet,
|
||||
exports: exports,
|
||||
@@ -294,7 +296,7 @@ extension Application {
|
||||
}
|
||||
unpackProgress.start()
|
||||
|
||||
var finalMessage = "Successfully built \(imageName)"
|
||||
var finalMessage = "Successfully built \(imageNames.joined(separator: ", "))"
|
||||
let taskManager = ProgressTaskCoordinator()
|
||||
// Currently, only a single export can be specified.
|
||||
for exp in exports {
|
||||
@@ -311,6 +313,12 @@ extension Application {
|
||||
for image in loaded {
|
||||
try Task.checkCancellation()
|
||||
try await image.unpack(platform: nil, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: unpackProgress.handler))
|
||||
|
||||
// Tag the unpacked image with all requested tags
|
||||
for tagName in imageNames {
|
||||
try Task.checkCancellation()
|
||||
_ = try await image.tag(new: tagName)
|
||||
}
|
||||
}
|
||||
case "tar":
|
||||
guard let dest = exp.destination else {
|
||||
@@ -349,8 +357,10 @@ extension Application {
|
||||
guard FileManager.default.fileExists(atPath: contextDir) else {
|
||||
throw ValidationError("context dir does not exist \(contextDir)")
|
||||
}
|
||||
guard let _ = try? Reference.parse(targetImageName) else {
|
||||
throw ValidationError("invalid reference \(targetImageName)")
|
||||
for name in targetImageNames {
|
||||
guard let _ = try? Reference.parse(name) else {
|
||||
throw ValidationError("invalid reference \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,23 @@ class TestCLIBuildBase: CLITest {
|
||||
otherArgs: [String] = []
|
||||
) throws -> String {
|
||||
try buildWithPaths(
|
||||
tag: tag,
|
||||
tags: [tag],
|
||||
tempContext: tempDir,
|
||||
tempDockerfileContext: tempDir,
|
||||
buildArgs: buildArgs,
|
||||
otherArgs: otherArgs
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func build(
|
||||
tags: [String],
|
||||
tempDir: URL,
|
||||
buildArgs: [String] = [],
|
||||
otherArgs: [String] = []
|
||||
) throws -> String {
|
||||
try buildWithPaths(
|
||||
tags: tags,
|
||||
tempContext: tempDir,
|
||||
tempDockerfileContext: tempDir,
|
||||
buildArgs: buildArgs,
|
||||
@@ -95,7 +111,7 @@ class TestCLIBuildBase: CLITest {
|
||||
// the dockerfile path. If both paths are the same, use `build` func above.
|
||||
@discardableResult
|
||||
func buildWithPaths(
|
||||
tag: String,
|
||||
tags: [String],
|
||||
tempContext: URL,
|
||||
tempDockerfileContext: URL,
|
||||
buildArgs: [String] = [],
|
||||
@@ -107,9 +123,11 @@ class TestCLIBuildBase: CLITest {
|
||||
"build",
|
||||
"-f",
|
||||
tempDockerfileContext.appendingPathComponent("Dockerfile").path,
|
||||
"-t",
|
||||
tag,
|
||||
]
|
||||
for tag in tags {
|
||||
args.append("-t")
|
||||
args.append(tag)
|
||||
}
|
||||
for arg in buildArgs {
|
||||
args.append("--build-arg")
|
||||
args.append(arg)
|
||||
|
||||
@@ -390,7 +390,7 @@ extension TestCLIBuildBase {
|
||||
|
||||
let imageName = "registry.local/build-diff-context:\(UUID().uuidString)"
|
||||
#expect(throws: Never.self) {
|
||||
try self.buildWithPaths(tag: imageName, tempContext: buildContextDir, tempDockerfileContext: dockerfileCtxDir)
|
||||
try self.buildWithPaths(tags: [imageName], tempContext: buildContextDir, tempDockerfileContext: dockerfileCtxDir)
|
||||
}
|
||||
#expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)")
|
||||
}
|
||||
@@ -432,5 +432,29 @@ extension TestCLIBuildBase {
|
||||
"expected platforms \(expected), got \(actual)"
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testBuildMultipleTags() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
let dockerfile: String =
|
||||
"""
|
||||
FROM scratch
|
||||
|
||||
ADD emptyFile /
|
||||
"""
|
||||
let context: [FileSystemEntry] = [.file("emptyFile", content: .zeroFilled(size: 1))]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let uuid = UUID().uuidString
|
||||
let tag1 = "registry.local/multi-tag-test:\(uuid)"
|
||||
let tag2 = "registry.local/multi-tag-test:latest"
|
||||
let tag3 = "registry.local/multi-tag-test:v1.0.0"
|
||||
|
||||
try self.build(tags: [tag1, tag2, tag3], tempDir: tempDir)
|
||||
|
||||
// Verify all three tags exist and point to the same image
|
||||
#expect(try self.inspectImage(tag1) == tag1, "expected to have successfully built \(tag1)")
|
||||
#expect(try self.inspectImage(tag2) == tag2, "expected to have successfully built \(tag2)")
|
||||
#expect(try self.inspectImage(tag3) == tag3, "expected to have successfully built \(tag3)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ container build [OPTIONS] [CONTEXT-DIR]
|
||||
* `--platform <platform>`: Add the platform to the build (takes precedence over --os and --arch)
|
||||
* `--progress <type>`: Progress type (format: auto|plain|tty)] (default: auto)
|
||||
* `-q, --quiet`: Suppress build output
|
||||
* `-t, --tag <name>`: Name for the built image
|
||||
* `-t, --tag <name>`: Name for the built image (can be specified multiple times)
|
||||
* `--target <stage>`: Set the target build stage
|
||||
* `--vsock-port <port>`: Builder shim vsock port (default: 8088)
|
||||
* `--version`: Show the version.
|
||||
@@ -122,6 +122,9 @@ container build --build-arg NODE_VERSION=18 -t my-app .
|
||||
|
||||
# build the production stage only and disable cache
|
||||
container build --target production --no-cache -t my-app:prod .
|
||||
|
||||
# build with multiple tags
|
||||
container build -t my-app:latest -t my-app:v1.0.0 -t my-app:stable .
|
||||
```
|
||||
|
||||
## Container Management
|
||||
|
||||
Reference in New Issue
Block a user