Fix using docker specific ignore with read-only build context (#1349)

This PR resolves #1343.
This PR depends on apple/container-builder-shim#72.

Do not create staging directory under build context, but pass
dockerignore file bytes to the container-builder-shim.

## Type of Change
- [x] Bug fix
- [ ] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
[Why is this change needed?]

## Testing
- [x] Tested locally
- [ ] Added/updated tests
- [ ] Added/updated docs
This commit is contained in:
jwhur
2026-04-20 11:58:43 -07:00
committed by GitHub
parent 49079da73e
commit 2ed65b9a53
4 changed files with 64 additions and 29 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ import PackageDescription
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.11.0"
let builderShimVersion = "0.12.0"
let scVersion = "0.30.1"
let package = Package(
+5 -5
View File
@@ -267,7 +267,7 @@ public struct Builder: Sendable {
public let secrets: [String: Data]
public let contextDir: String
public let dockerfile: Data
public let hiddenDockerDir: String?
public let dockerignore: Data?
public let labels: [String]
public let noCache: Bool
public let platforms: [Platform]
@@ -287,7 +287,7 @@ public struct Builder: Sendable {
secrets: [String: Data],
contextDir: String,
dockerfile: Data,
hiddenDockerDir: String?,
dockerignore: Data?,
labels: [String],
noCache: Bool,
platforms: [Platform],
@@ -306,7 +306,7 @@ public struct Builder: Sendable {
self.secrets = secrets
self.contextDir = contextDir
self.dockerfile = dockerfile
self.hiddenDockerDir = hiddenDockerDir
self.dockerignore = dockerignore
self.labels = labels
self.noCache = noCache
self.platforms = platforms
@@ -329,8 +329,8 @@ public struct Builder: Sendable {
metadata.addString(config.terminal != nil ? "tty" : "plain", forKey: "progress")
metadata.addString(config.target, forKey: "target")
if let hiddenDockerDir = config.hiddenDockerDir {
metadata.addString(hiddenDockerDir, forKey: "hidden-docker-dir")
if let dockerignore = config.dockerignore {
metadata.addString(dockerignore.base64EncodedString(), forKey: "dockerignore")
}
for tag in config.tags {
metadata.addString(tag, forKey: "tag")
+2 -23
View File
@@ -28,8 +28,6 @@ import TerminalProgress
extension Application {
public struct BuildCommand: AsyncLoggableCommand {
private static let hiddenDockerDir = ".com.apple.container.dockerfiles"
public init() {}
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
@@ -220,7 +218,6 @@ extension Application {
let buildFileData: Data
var ignoreFileData: Data? = nil
var hiddenDockerDir: String? = nil
// Dockerfile should be read from stdin
if dockerfile == "-" {
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("Dockerfile-\(UUID().uuidString)")
@@ -248,24 +245,6 @@ extension Application {
let ignoreFileURL = URL(filePath: dockerfile + ".dockerignore")
buildFileData = try Data(contentsOf: URL(filePath: dockerfile))
ignoreFileData = try? Data(contentsOf: ignoreFileURL)
if var ignoreFileData {
hiddenDockerDir = Self.hiddenDockerDir
let hiddenDirInContext = URL(fileURLWithPath: contextDir).appendingPathComponent(Self.hiddenDockerDir)
try FileManager.default.createDirectory(at: hiddenDirInContext, withIntermediateDirectories: true)
try buildFileData.write(to: hiddenDirInContext.appendingPathComponent("Dockerfile"))
ignoreFileData.append("\n\(Self.hiddenDockerDir)".data(using: .utf8) ?? Data())
try ignoreFileData.write(to: hiddenDirInContext.appendingPathComponent("Dockerfile.dockerignore"))
}
}
defer {
if let hiddenDockerDir {
let hiddenDirInContext = URL(fileURLWithPath: contextDir).appendingPathComponent(hiddenDockerDir)
try? FileManager.default.removeItem(at: hiddenDirInContext)
}
}
let secretsData: [String: Data] = try self.secrets.mapValues { secret in
@@ -351,7 +330,7 @@ extension Application {
return results
}()
group.addTask {
[terminal, buildArg, secretsData, contextDir, hiddenDockerDir, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log] in
[terminal, buildArg, secretsData, contextDir, ignoreFileData, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log] in
let config = Builder.BuildConfig(
buildID: buildID,
contentStore: RemoteContentStoreClient(),
@@ -359,7 +338,7 @@ extension Application {
secrets: secretsData,
contextDir: contextDir,
dockerfile: buildFileData,
hiddenDockerDir: hiddenDockerDir,
dockerignore: ignoreFileData,
labels: label,
noCache: noCache,
platforms: [Platform](platforms),
@@ -1173,6 +1173,62 @@ extension TestCLIBuildBase {
#expect(includedResult.status == 0, "included.txt should be present")
}
// Test: Build context is read-only; Dockerfile and Dockerfile.dockerignore live outside the context
@Test func testDockerIgnoreReadonlyContext() throws {
let tempDir: URL = try createTempDir()
let contextDir = tempDir.appendingPathComponent("context")
defer {
// Restore write permission so the directory can be removed
try? FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: contextDir.path
)
try! FileManager.default.removeItem(at: tempDir)
}
let dockerfile =
"""
FROM ghcr.io/linuxcontainers/alpine:3.20
WORKDIR /app
COPY . .
"""
// Context contains two files; Dockerfile and Dockerfile.dockerignore are placed outside
// the context directory (co-located in tempDir).
let context: [FileSystemEntry] = [
.file("included.txt", content: .data("This file should be included.\n".data(using: .utf8)!)),
.file("secret.txt", content: .data("This file should be excluded by Dockerfile.dockerignore.\n".data(using: .utf8)!)),
]
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
// Write Dockerfile.dockerignore next to Dockerfile, both outside the context directory
let dockerignoreData = "secret.txt\n".data(using: .utf8)!
try dockerignoreData.write(to: tempDir.appendingPathComponent("Dockerfile.dockerignore"), options: .atomic)
// Make the context directory read-only before building
try FileManager.default.setAttributes(
[.posixPermissions: 0o555],
ofItemAtPath: contextDir.path
)
let dockerfilePath = tempDir.appendingPathComponent("Dockerfile")
let imageName = "registry.local/dockerignore-readonly-context:\(UUID().uuidString.prefix(6))"
let args = ["build", "-f", dockerfilePath.path, "-t", imageName, contextDir.path]
let response = try run(arguments: args)
if response.status != 0 {
throw CLIError.executionFailed("build failed: stdout=\(response.output) stderr=\(response.error)")
}
let containerName = "dockerignore-readonly-context-\(UUID().uuidString.prefix(6))"
try self.doLongRun(name: containerName, image: imageName)
defer { try? self.doStop(name: containerName) }
let includedResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/included.txt"])
#expect(includedResult.status == 0, "included.txt should be present")
let secretResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/secret.txt"])
#expect(secretResult.status != 0, "secret.txt should NOT be present (excluded by Dockerfile.dockerignore)")
}
@Test func testNonExistingDockerfile() throws {
let tempDir: URL = try createTempDir()
defer {