Skip to content

Commit 4a08286

Browse files
committed
Add --iidfile option to container build
Writes the built image's digest to a file after a successful build, mirroring the existing --cidfile convention used by container run/create. Only supported with the default OCI output (type=oci), since that is the only export that produces a loaded image digest. Fixes #1998
1 parent d1d7635 commit 4a08286

4 files changed

Lines changed: 84 additions & 1 deletion

File tree

Sources/ContainerCommands/BuildCommand.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ extension Application {
8181

8282
var dockerfile: String = "-"
8383

84+
@Option(name: .long, help: ArgumentHelp("Write the image ID to the file", valueName: "path"))
85+
var iidfile: String = ""
86+
8487
@Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val"))
8588
var label: [String] = []
8689

@@ -350,6 +353,7 @@ extension Application {
350353
group.addTask {
351354
[
352355
terminal, buildArg, secretsData, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log,
356+
iidfile,
353357
] in
354358
let config = Builder.BuildConfig(
355359
buildID: buildID,
@@ -389,6 +393,7 @@ extension Application {
389393
unpackProgress.start()
390394

391395
var finalMessage = imageNames.joined(separator: "\n")
396+
var builtImageDigest: String?
392397
let taskManager = ProgressTaskCoordinator()
393398
// Currently, only a single export can be specified.
394399
for exp in exports {
@@ -408,6 +413,7 @@ extension Application {
408413
for image in result.images {
409414
try Task.checkCancellation()
410415
try await image.unpack(platform: nil, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: unpackProgress.handler))
416+
builtImageDigest = image.digest
411417

412418
// Tag the unpacked image with all requested tags
413419
for tagName in imageNames {
@@ -439,6 +445,19 @@ extension Application {
439445
}
440446
await taskManager.finish()
441447
unpackProgress.finish()
448+
449+
if !iidfile.isEmpty {
450+
guard let builtImageDigest else {
451+
throw ContainerizationError(.internalError, message: "no image digest available to write to iidfile")
452+
}
453+
let data = builtImageDigest.data(using: .utf8)
454+
var attributes = [FileAttributeKey: Any]()
455+
attributes[.posixPermissions] = 0o644
456+
guard FileManager.default.createFile(atPath: iidfile, contents: data, attributes: attributes) else {
457+
throw ContainerizationError(.internalError, message: "failed to create iidfile at \(iidfile)")
458+
}
459+
}
460+
442461
print(finalMessage)
443462
}
444463

@@ -460,6 +479,20 @@ extension Application {
460479
}
461480
}
462481

482+
if !iidfile.isEmpty {
483+
for exportSpec in output {
484+
let export: Builder.BuildExport
485+
do {
486+
export = try Builder.BuildExport(from: exportSpec)
487+
} catch {
488+
throw ValidationError("invalid output \(exportSpec): \(error)")
489+
}
490+
guard export.type == "oci" else {
491+
throw ValidationError("--iidfile requires the default OCI output (type=oci)")
492+
}
493+
}
494+
}
495+
463496
switch file {
464497
case "-":
465498
dockerfile = "-"

Sources/ContainerTestSupport/ContainerFixture+ImageHelpers.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import Testing
2222
extension ContainerFixture {
2323
/// Decoded output of `container image inspect` or `container image list --format json`.
2424
public struct ImageInspectOutput: Codable {
25-
public struct Configuration: Codable { public let name: String }
25+
public struct Configuration: Codable {
26+
public struct Descriptor: Codable { public let digest: String }
27+
public let name: String
28+
public let descriptor: Descriptor
29+
}
2630
public struct Variant: Codable {
2731
public struct Platform: Codable {
2832
public let os: String

Tests/IntegrationTests/Build/TestCLIBuilder.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,51 @@ struct TestCLIBuilder {
403403
}
404404
}
405405

406+
@Test func testBuildIidfile() async throws {
407+
try await ContainerFixture.with { f in
408+
let dir = try f.createTempDir()
409+
try f.createContext(
410+
dir: dir,
411+
dockerfile: "FROM scratch\nADD emptyFile /",
412+
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
413+
let tag = "registry.local/iidfile-test:\(UUID().uuidString)"
414+
let iidfile = dir.appending("image.id")
415+
416+
try f.buildWithPaths(tags: [tag], contextDir: dir, otherArgs: ["--iidfile", iidfile.string])
417+
418+
#expect(FileManager.default.fileExists(atPath: iidfile.string), "iidfile should be created")
419+
let writtenDigest = try String(contentsOfFile: iidfile.string, encoding: .utf8)
420+
#expect(!writtenDigest.hasSuffix("\n"), "iidfile should not contain a trailing newline")
421+
#expect(writtenDigest.hasPrefix("sha256:"), "iidfile should contain a digest")
422+
423+
let inspected = try f.doInspectImages(tag)
424+
#expect(inspected.count == 1)
425+
#expect(writtenDigest == inspected[0].configuration.descriptor.digest)
426+
}
427+
}
428+
429+
@Test func testBuildIidfileRejectsNonOCIOutput() async throws {
430+
try await ContainerFixture.with { f in
431+
let dir = try f.createTempDir()
432+
try f.createContext(
433+
dir: dir,
434+
dockerfile: "FROM scratch\nADD emptyFile /",
435+
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
436+
let iidfile = dir.appending("image.id")
437+
let exportPath = dir.appending("export.tar")
438+
439+
let result = try f.run([
440+
"build",
441+
"-f", dir.appending("Dockerfile").string,
442+
"-o", "type=tar,dest=\(exportPath.string)",
443+
"--iidfile", iidfile.string,
444+
dir.appending("context").string,
445+
])
446+
#expect(result.status != 0, "build should reject --iidfile combined with a non-OCI output")
447+
#expect(!FileManager.default.fileExists(atPath: iidfile.string), "iidfile should not be created")
448+
}
449+
}
450+
406451
@Test func testBuildAfterContextChange() async throws {
407452
try await ContainerFixture.with { f in
408453
let dir = try f.createTempDir()

docs/command-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ container build [<options>] [<context-dir>]
147147
* `--dns-option <option>`: DNS options
148148
* `--dns-search <domain>`: DNS search domains
149149
* `-f, --file <path>`: Path to Dockerfile
150+
* `--iidfile <path>`: Write the image ID to the file
150151
* `-l, --label <key=val>`: Set a label
151152
* `-m, --memory <memory>`: Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix (default: 2048MB)
152153
* `--no-cache`: Do not use cache

0 commit comments

Comments
 (0)