mirror of
https://github.com/apple/container.git
synced 2026-11-05 21:43:24 +00:00
Fix empty followpaths handling in BuildFSSync (#2414)
- Resolves #2405 in conjunction with apple/container-builder-shim#105 which resolves the root cause in the builder shim and #2434 which brings in the new shim. - When BuildKit evaluates instructions requiring the full build context (such as steps with tmpfs mounts), it emits a Walk request with empty or omitted followpaths metadata. BuildFSSync previously defaulted missing followpaths to [], resulting in no context files being packed into the archive. - This change defaults omitted or empty followpaths to ["."], ensuring the entire context is served as expected by BuildKit's filesync protocol.
This commit is contained in:
1 parent
410b29fe8b
commit
305cb6ed0a
3 files changed
+71
-1
No files matched your search
@@ -233,6 +233,9 @@ actor BuildFSSync: BuildPipelineHandler {
|
||||
/// This is the primary data path for build-context transfer. BuildKit sends
|
||||
/// a `Walk` request whose `followpaths` field names the context paths needed
|
||||
/// for the current build step (e.g. the source of a `COPY` instruction).
|
||||
/// If `followpaths` is omitted or empty (meaning the build step requires the
|
||||
/// entire context, such as a step with a tmpfs mount), all files in the
|
||||
/// context root are included.
|
||||
/// The host resolves those globs, builds an entry set, and passes it to
|
||||
/// `Archiver.compress` to produce the tar.
|
||||
///
|
||||
@@ -253,7 +256,7 @@ actor BuildFSSync: BuildPipelineHandler {
|
||||
let wantsTar = packet.mode() == "tar"
|
||||
|
||||
var entries: [String: Set<DirEntry>] = [:]
|
||||
let followPaths: [String] = packet.followPaths() ?? []
|
||||
let followPaths: [String] = packet.followPaths().flatMap { $0.isEmpty ? nil : $0 } ?? ["."]
|
||||
|
||||
let followPathsWalked = try walk(root: self.contextDir, includePatterns: followPaths)
|
||||
for url in followPathsWalked {
|
||||
|
||||
@@ -534,6 +534,53 @@ import Testing
|
||||
let secretLeak = infos.first { $0.name.hasSuffix("secret.txt") }
|
||||
#expect(secretLeak == nil, "no entry for the external file should appear in walk() results: \(infos.map { $0.name })")
|
||||
}
|
||||
|
||||
// MARK: - walk(): empty or omitted followpaths requests entire context
|
||||
//
|
||||
// BuildKit omits followpaths when a build step needs the whole context
|
||||
// (for example, RUN --mount=type=tmpfs). An empty or omitted followpaths
|
||||
// must result in the entire context being served rather than an empty
|
||||
// result.
|
||||
|
||||
@Test func testWalkJSONEmptyFollowPathsIncludesEntireContext() async throws {
|
||||
try write("hello", to: contextDir.appendingPathComponent("a.txt"))
|
||||
try write("world", to: contextDir.appendingPathComponent("b.txt"))
|
||||
|
||||
let fssync = try BuildFSSync(contextDir)
|
||||
let infos = try await walkJSON(fssync, followPaths: [])
|
||||
|
||||
#expect(infos.contains { $0.name == "a.txt" }, "a.txt should be included when followpaths is empty")
|
||||
#expect(infos.contains { $0.name == "b.txt" }, "b.txt should be included when followpaths is empty")
|
||||
}
|
||||
|
||||
@Test func testWalkJSONMissingFollowPathsMetadataIncludesEntireContext() async throws {
|
||||
try write("hello", to: contextDir.appendingPathComponent("a.txt"))
|
||||
try write("world", to: contextDir.appendingPathComponent("b.txt"))
|
||||
|
||||
let fssync = try BuildFSSync(contextDir)
|
||||
var p = BuildTransfer()
|
||||
p.id = UUID().uuidString
|
||||
p.source = "."
|
||||
p.metadata = [
|
||||
"mode": "json"
|
||||
]
|
||||
|
||||
var continuation: AsyncStream<ClientStream>.Continuation!
|
||||
let stream = AsyncStream<ClientStream> { continuation = $0 }
|
||||
try await fssync.walk(continuation, p, "build-0")
|
||||
continuation.finish()
|
||||
|
||||
var fileInfos: [BuildFSSync.FileInfo] = []
|
||||
for await resp in stream {
|
||||
let data = resp.buildTransfer.data
|
||||
if !data.isEmpty {
|
||||
fileInfos += try JSONDecoder().decode([BuildFSSync.FileInfo].self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
#expect(fileInfos.contains { $0.name == "a.txt" }, "a.txt should be included when followpaths metadata is omitted")
|
||||
#expect(fileInfos.contains { $0.name == "b.txt" }, "b.txt should be included when followpaths metadata is omitted")
|
||||
}
|
||||
}
|
||||
|
||||
private final class ManagedAtomicFlag: @unchecked Sendable {
|
||||
|
||||
@@ -496,6 +496,26 @@ struct TestCLIBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRunWithTmpfsMount() async throws {
|
||||
try await ContainerFixture.with { f in
|
||||
let dir = try f.createTempDir()
|
||||
try f.createContext(
|
||||
dir: dir,
|
||||
dockerfile: """
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
COPY a.txt b.txt /out/
|
||||
RUN --mount=type=tmpfs,dst=/scratch ls -l /out
|
||||
""",
|
||||
context: [
|
||||
.file("a.txt", content: .data("one\n".data(using: .utf8)!)),
|
||||
.file("b.txt", content: .data("two\n".data(using: .utf8)!)),
|
||||
])
|
||||
let image = "registry.local/tmpfs-mount-test:\(UUID().uuidString)"
|
||||
try f.build(tag: image, contextDir: dir)
|
||||
try f.assertImageBuilt(image)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - .dockerignore tests
|
||||
|
||||
@Test func testBuildDockerIgnore() async throws {
|
||||
|
||||
Reference in new issue
Block a user