Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Sources/ContainerCommands/BuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ extension Application {

var dockerfile: String = "-"

@Option(name: .long, help: ArgumentHelp("Write the image ID to the file", valueName: "path"))
var iidfile: String = ""

@Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val"))
var label: [String] = []

Expand Down Expand Up @@ -350,6 +353,7 @@ extension Application {
group.addTask {
[
terminal, buildArg, secretsData, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log,
iidfile,
] in
let config = Builder.BuildConfig(
buildID: buildID,
Expand Down Expand Up @@ -389,6 +393,7 @@ extension Application {
unpackProgress.start()

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

// Tag the unpacked image with all requested tags
for tagName in imageNames {
Expand Down Expand Up @@ -439,6 +445,19 @@ extension Application {
}
await taskManager.finish()
unpackProgress.finish()

if !iidfile.isEmpty {
guard let builtImageDigest else {
throw ContainerizationError(.internalError, message: "no image digest available to write to iidfile")
}
let data = builtImageDigest.data(using: .utf8)
var attributes = [FileAttributeKey: Any]()
attributes[.posixPermissions] = 0o644
guard FileManager.default.createFile(atPath: iidfile, contents: data, attributes: attributes) else {
throw ContainerizationError(.internalError, message: "failed to create iidfile at \(iidfile)")
}
}

print(finalMessage)
}

Expand All @@ -460,6 +479,20 @@ extension Application {
}
}

if !iidfile.isEmpty {
for exportSpec in output {
let export: Builder.BuildExport
do {
export = try Builder.BuildExport(from: exportSpec)
} catch {
throw ValidationError("invalid output \(exportSpec): \(error)")
}
guard export.type == "oci" else {
throw ValidationError("--iidfile requires the default OCI output (type=oci)")
}
}
}

switch file {
case "-":
dockerfile = "-"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import Testing
extension ContainerFixture {
/// Decoded output of `container image inspect` or `container image list --format json`.
public struct ImageInspectOutput: Codable {
public struct Configuration: Codable { public let name: String }
public struct Configuration: Codable {
public struct Descriptor: Codable { public let digest: String }
public let name: String
public let descriptor: Descriptor
}
public struct Variant: Codable {
public struct Platform: Codable {
public let os: String
Expand Down
45 changes: 45 additions & 0 deletions Tests/IntegrationTests/Build/TestCLIBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,51 @@ struct TestCLIBuilder {
}
}

@Test func testBuildIidfile() async throws {
try await ContainerFixture.with { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM scratch\nADD emptyFile /",
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
let tag = "registry.local/iidfile-test:\(UUID().uuidString)"
let iidfile = dir.appending("image.id")

try f.buildWithPaths(tags: [tag], contextDir: dir, otherArgs: ["--iidfile", iidfile.string])

#expect(FileManager.default.fileExists(atPath: iidfile.string), "iidfile should be created")
let writtenDigest = try String(contentsOfFile: iidfile.string, encoding: .utf8)
#expect(!writtenDigest.hasSuffix("\n"), "iidfile should not contain a trailing newline")
#expect(writtenDigest.hasPrefix("sha256:"), "iidfile should contain a digest")

let inspected = try f.doInspectImages(tag)
#expect(inspected.count == 1)
#expect(writtenDigest == inspected[0].configuration.descriptor.digest)
}
}

@Test func testBuildIidfileRejectsNonOCIOutput() async throws {
try await ContainerFixture.with { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM scratch\nADD emptyFile /",
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
let iidfile = dir.appending("image.id")
let exportPath = dir.appending("export.tar")

let result = try f.run([
"build",
"-f", dir.appending("Dockerfile").string,
"-o", "type=tar,dest=\(exportPath.string)",
"--iidfile", iidfile.string,
dir.appending("context").string,
])
#expect(result.status != 0, "build should reject --iidfile combined with a non-OCI output")
#expect(!FileManager.default.fileExists(atPath: iidfile.string), "iidfile should not be created")
}
}

@Test func testBuildAfterContextChange() async throws {
try await ContainerFixture.with { f in
let dir = try f.createTempDir()
Expand Down
1 change: 1 addition & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ container build [<options>] [<context-dir>]
* `--dns-option <option>`: DNS options
* `--dns-search <domain>`: DNS search domains
* `-f, --file <path>`: Path to Dockerfile
* `--iidfile <path>`: Write the image ID to the file
* `-l, --label <key=val>`: Set a label
* `-m, --memory <memory>`: Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix (default: 2048MB)
* `--no-cache`: Do not use cache
Expand Down