Skip to content

Commit fabfc55

Browse files
JaewonHurRonitsabhaya75jglogan
authored
Send tar hash in the first BuildTransfer packet (apple#1149)
Send the hash of entire tar file in the first BuildTransfer packet to prevent container-builder-shim from using stale cached contents. This PR resolves apple#1143. This PR relies on apple/container-builder-shim#64. ## Type of Change - [X] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Current container-builder-shim uses only first few bytes of tar file as checksum, which leads to the usage of stale cached contents if the change of build context is not included in the first bytes of tar file. ## Testing - [X] Tested locally - [ ] Added/updated tests - [ ] Added/updated docs --------- Co-authored-by: Ronit Sabhaya <ronitsabhaya75@gmail.com> Co-authored-by: J Logan <john_logan@apple.com>
1 parent 6e9b8d7 commit fabfc55

4 files changed

Lines changed: 91 additions & 7 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import PackageDescription
2222

2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
25-
let builderShimVersion = "0.7.0"
25+
let builderShimVersion = "0.8.0"
2626
let scVersion = "0.25.0"
2727

2828
let package = Package(

Sources/ContainerBuild/BuildFSSync.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Collections
1818
import ContainerAPIClient
1919
import ContainerizationArchive
2020
import ContainerizationOCI
21+
import CryptoKit
2122
import Foundation
2223
import GRPC
2324

@@ -199,7 +200,7 @@ actor BuildFSSync: BuildPipelineHandler {
199200
format: .paxRestricted,
200201
filter: .none)
201202

202-
try Archiver.compress(
203+
let tarHash = try Archiver.compress(
203204
source: contextDir,
204205
destination: tarURL,
205206
writerConfiguration: writerCfg
@@ -229,6 +230,25 @@ actor BuildFSSync: BuildPipelineHandler {
229230
pathInArchive: URL(fileURLWithPath: rel))
230231
}
231232

233+
let hash = tarHash.compactMap { String(format: "%02x", $0) }.joined()
234+
let header = BuildTransfer(
235+
id: packet.id,
236+
source: tarURL.path,
237+
complete: false,
238+
isDir: false,
239+
metadata: [
240+
"os": "linux",
241+
"stage": "fssync",
242+
"mode": "tar",
243+
"hash": hash,
244+
]
245+
)
246+
var resp = ClientStream()
247+
resp.buildID = buildID
248+
resp.buildTransfer = header
249+
resp.packetType = .buildTransfer(header)
250+
sender.yield(resp)
251+
232252
for try await chunk in try tarURL.bufferedCopyReader() {
233253
let part = BuildTransfer(
234254
id: packet.id,

Sources/Services/ContainerAPIService/Client/Archiver.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
import ContainerizationArchive
1818
import ContainerizationOS
19+
import CryptoKit
1920
import Foundation
2021

2122
public final class Archiver: Sendable {
22-
public struct ArchiveEntryInfo: Sendable {
23+
public struct ArchiveEntryInfo: Sendable, Codable {
2324
public let pathOnHost: URL
2425
public let pathInArchive: URL
2526

@@ -48,13 +49,15 @@ public final class Archiver: Sendable {
4849
followSymlinks: Bool = false,
4950
writerConfiguration: ArchiveWriterConfiguration = ArchiveWriterConfiguration(format: .paxRestricted, filter: .gzip),
5051
closure: (URL) -> ArchiveEntryInfo?
51-
) throws {
52+
) throws -> SHA256.Digest {
5253
let source = source.standardizedFileURL
5354
let destination = destination.standardizedFileURL
5455

5556
let fileManager = FileManager.default
5657
try? fileManager.removeItem(at: destination)
5758

59+
var hasher = SHA256()
60+
5861
do {
5962
let directory = destination.deletingLastPathComponent()
6063
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
@@ -71,7 +74,8 @@ public final class Archiver: Sendable {
7174
entryInfo.append(info)
7275
}
7376
} else {
74-
while let relPath = enumerator.nextObject() as? String {
77+
let relPaths = enumerator.compactMap { $0 as? String }
78+
for relPath in relPaths.sorted(by: { $0 < $1 }) {
7579
let url = source.appending(path: relPath).standardizedFileURL
7680
guard let info = closure(url) else {
7781
continue
@@ -85,17 +89,23 @@ public final class Archiver: Sendable {
8589
)
8690
try archiver.open(file: destination)
8791

92+
let encoder = JSONEncoder()
93+
encoder.outputFormatting = .sortedKeys
94+
8895
for info in entryInfo {
8996
guard let entry = try Self._createEntry(entryInfo: info) else {
9097
throw Error.failedToCreateEntry
9198
}
92-
try Self._compressFile(item: info.pathOnHost, entry: entry, archiver: archiver)
99+
hasher.update(data: try encoder.encode(info))
100+
try Self._compressFile(item: info.pathOnHost, entry: entry, archiver: archiver, hasher: &hasher)
93101
}
94102
try archiver.finishEncoding()
95103
} catch {
96104
try? fileManager.removeItem(at: destination)
97105
throw error
98106
}
107+
108+
return hasher.finalize()
99109
}
100110

101111
public static func uncompress(source: URL, destination: URL) throws {
@@ -186,7 +196,7 @@ public final class Archiver: Sendable {
186196
}
187197

188198
// MARK: private functions
189-
private static func _compressFile(item: URL, entry: WriteEntry, archiver: ArchiveWriter) throws {
199+
private static func _compressFile(item: URL, entry: WriteEntry, archiver: ArchiveWriter, hasher: inout SHA256) throws {
190200
guard let stream = InputStream(url: item) else {
191201
return
192202
}
@@ -204,6 +214,7 @@ public final class Archiver: Sendable {
204214
break
205215
} else {
206216
let data = Data(bytes: readBuffer, count: byteRead)
217+
hasher.update(data: data)
207218
try data.withUnsafeBytes { pointer in
208219
try writer.writeChunk(data: pointer)
209220
}

Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,59 @@ extension TestCLIBuildBase {
457457
#expect(try self.inspectImage(tag3) == tag3, "expected to have successfully built \(tag3)")
458458
}
459459

460+
@Test func testBuildAfterContextChange() throws {
461+
let name = "test-build-context-change"
462+
let tempDir: URL = try createTempDir()
463+
464+
// Create initial context with file "foo" containing "initial"
465+
let dockerfile =
466+
"""
467+
FROM ghcr.io/linuxcontainers/alpine:3.20
468+
COPY foo /foo
469+
COPY bar /bar
470+
"""
471+
let initialContent = "initial".data(using: .utf8)!
472+
let context: [FileSystemEntry] = [
473+
.file("foo", content: .data(Data((0..<4 * 1024 * 1024).map { UInt8($0 % 256) }))),
474+
.file("bar", content: .data(initialContent)),
475+
]
476+
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
477+
478+
// Build first image
479+
let imageName1 = "\(name):v1"
480+
let containerName1 = "\(name)-container-v1"
481+
try self.build(tag: imageName1, tempDir: tempDir)
482+
#expect(try self.inspectImage(imageName1) == imageName1, "expected to have successfully built \(imageName1)")
483+
484+
// Run container and verify content is "initial"
485+
try self.doLongRun(name: containerName1, image: imageName1)
486+
defer {
487+
try? self.doStop(name: containerName1)
488+
}
489+
var output = try doExec(name: containerName1, cmd: ["cat", "/bar"])
490+
#expect(output == "initial", "expected file contents to be 'initial', instead got '\(output)'")
491+
492+
// Update the file "foo" to contain "updated"
493+
let updatedContent = "updated".data(using: .utf8)!
494+
let contextDir = tempDir.appendingPathComponent("context")
495+
let barPath = contextDir.appendingPathComponent("bar")
496+
try updatedContent.write(to: barPath, options: .atomic)
497+
498+
// Build second image
499+
let imageName2 = "\(name):v2"
500+
let containerName2 = "\(name)-container-v2"
501+
try self.build(tag: imageName2, tempDir: tempDir)
502+
#expect(try self.inspectImage(imageName2) == imageName2, "expected to have successfully built \(imageName2)")
503+
504+
// Run container and verify content is "updated"
505+
try self.doLongRun(name: containerName2, image: imageName2)
506+
defer {
507+
try? self.doStop(name: containerName2)
508+
}
509+
output = try doExec(name: containerName2, cmd: ["cat", "/bar"])
510+
#expect(output == "updated", "expected file contents to be 'updated', instead got '\(output)'")
511+
}
512+
460513
@Test func testBuildWithDockerfileFromStdin() throws {
461514
let tempDir: URL = try createTempDir()
462515
let dockerfile =

0 commit comments

Comments
 (0)