Skip to content

Commit 010cc3a

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 ddaf2ca commit 010cc3a

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

Sources/ContainerCommands/BuildCommand.swift

Lines changed: 33 additions & 1 deletion
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

@@ -371,7 +374,7 @@ extension Application {
371374
group.addTask {
372375
[
373376
terminal, buildArg, secretsData, ssh, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL,
374-
log
377+
log, iidfile,
375378
] in
376379
let config = Builder.BuildConfig(
377380
buildID: buildID,
@@ -412,6 +415,7 @@ extension Application {
412415
unpackProgress.start()
413416

414417
var finalMessage = imageNames.joined(separator: "\n")
418+
var builtImageDigest: String?
415419
let taskManager = ProgressTaskCoordinator()
416420
// Currently, only a single export can be specified.
417421
for exp in exports {
@@ -431,6 +435,7 @@ extension Application {
431435
for image in result.images {
432436
try Task.checkCancellation()
433437
try await image.unpack(platform: nil, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: unpackProgress.handler))
438+
builtImageDigest = image.digest
434439

435440
// Tag the unpacked image with all requested tags
436441
for tagName in imageNames {
@@ -462,6 +467,19 @@ extension Application {
462467
}
463468
await taskManager.finish()
464469
unpackProgress.finish()
470+
471+
if !iidfile.isEmpty {
472+
guard let builtImageDigest else {
473+
throw ContainerizationError(.internalError, message: "no image digest available to write to iidfile")
474+
}
475+
let data = builtImageDigest.data(using: .utf8)
476+
var attributes = [FileAttributeKey: Any]()
477+
attributes[.posixPermissions] = 0o644
478+
guard FileManager.default.createFile(atPath: iidfile, contents: data, attributes: attributes) else {
479+
throw ContainerizationError(.internalError, message: "failed to create iidfile at \(iidfile)")
480+
}
481+
}
482+
465483
print(finalMessage)
466484
}
467485

@@ -483,6 +501,20 @@ extension Application {
483501
}
484502
}
485503

504+
if !iidfile.isEmpty {
505+
for exportSpec in output {
506+
let export: Builder.BuildExport
507+
do {
508+
export = try Builder.BuildExport(from: exportSpec)
509+
} catch {
510+
throw ValidationError("invalid output \(exportSpec): \(error)")
511+
}
512+
guard export.type == "oci" else {
513+
throw ValidationError("--iidfile requires the default OCI output (type=oci)")
514+
}
515+
}
516+
}
517+
486518
switch file {
487519
case "-":
488520
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
@@ -149,6 +149,7 @@ container build [<options>] [<context-dir>]
149149
* `--dns-option <option>`: DNS options
150150
* `--dns-search <domain>`: DNS search domains
151151
* `-f, --file <path>`: Path to Dockerfile
152+
* `--iidfile <path>`: Write the image ID to the file
152153
* `-l, --label <key=val>`: Set a label
153154
* `-m, --memory <memory>`: Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix (default: 2048MB)
154155
* `--no-cache`: Do not use cache

0 commit comments

Comments
 (0)