mirror of
https://github.com/apple/container.git
synced 2026-09-25 17:15:38 +00:00
Fix docker ignore (#1273)
This PR fixes `container build` to use docker specific ignore (#1169). This PR relies on apple/container-builder-shim#68. When docker specific ignore file is detected, it creates a hidden docker directory (i.e., `.hidden-docker-dir`) under the build context, and places Dockerfile and ignore file under there. This hidden directory is passed to the builder shim and used by buildkit daemon to correctly figure out the docker specific ignore file. Docker specific ignore file is detected regardless of whether i) they are under nested directory, ii) outside build context, iii) using custom names. If docker specific ignore is not provided, hidden directory is also not created, and buildkit daemon will use the docker ignore file at build context root as usual. ## Type of Change - [X] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Docker specific ignore works. ## Testing - [X] Tested locally - [ ] Added/updated tests - [ ] Added/updated docs
This commit is contained in:
+1
-1
@@ -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.8.0"
|
||||
let builderShimVersion = "0.10.0"
|
||||
let scVersion = "0.27.0"
|
||||
|
||||
let package = Package(
|
||||
|
||||
@@ -266,6 +266,7 @@ public struct Builder: Sendable {
|
||||
public let buildArgs: [String]
|
||||
public let contextDir: String
|
||||
public let dockerfile: Data
|
||||
public let hiddenDockerDir: String?
|
||||
public let labels: [String]
|
||||
public let noCache: Bool
|
||||
public let platforms: [Platform]
|
||||
@@ -284,6 +285,7 @@ public struct Builder: Sendable {
|
||||
buildArgs: [String],
|
||||
contextDir: String,
|
||||
dockerfile: Data,
|
||||
hiddenDockerDir: String?,
|
||||
labels: [String],
|
||||
noCache: Bool,
|
||||
platforms: [Platform],
|
||||
@@ -301,6 +303,7 @@ public struct Builder: Sendable {
|
||||
self.buildArgs = buildArgs
|
||||
self.contextDir = contextDir
|
||||
self.dockerfile = dockerfile
|
||||
self.hiddenDockerDir = hiddenDockerDir
|
||||
self.labels = labels
|
||||
self.noCache = noCache
|
||||
self.platforms = platforms
|
||||
@@ -323,6 +326,9 @@ 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")
|
||||
}
|
||||
for tag in config.tags {
|
||||
metadata.addString(tag, forKey: "tag")
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ 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()
|
||||
@@ -72,6 +74,8 @@ extension Application {
|
||||
@Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path"))
|
||||
var file: String?
|
||||
|
||||
var dockerfile: String = "-"
|
||||
|
||||
@Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val"))
|
||||
var label: [String] = []
|
||||
|
||||
@@ -204,24 +208,11 @@ extension Application {
|
||||
throw ValidationError("builder is not running")
|
||||
}
|
||||
|
||||
let buildFilePath: String
|
||||
if let file = self.file {
|
||||
buildFilePath = file
|
||||
} else {
|
||||
guard
|
||||
let resolvedPath = try BuildFile.resolvePath(
|
||||
contextDir: self.contextDir,
|
||||
log: log
|
||||
)
|
||||
else {
|
||||
throw ValidationError("failed to find Dockerfile or Containerfile in the context directory \(self.contextDir)")
|
||||
}
|
||||
buildFilePath = resolvedPath
|
||||
}
|
||||
|
||||
let buildFileData: Data
|
||||
var ignoreFileData: Data? = nil
|
||||
var hiddenDockerDir: String? = nil
|
||||
// Dockerfile should be read from stdin
|
||||
if file == "-" {
|
||||
if dockerfile == "-" {
|
||||
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("Dockerfile-\(UUID().uuidString)")
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: tempFile)
|
||||
@@ -244,7 +235,27 @@ extension Application {
|
||||
try fileHandle.close()
|
||||
buildFileData = try Data(contentsOf: URL(filePath: tempFile.path()))
|
||||
} else {
|
||||
buildFileData = try Data(contentsOf: URL(filePath: buildFilePath))
|
||||
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 systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
@@ -320,13 +331,14 @@ extension Application {
|
||||
}
|
||||
return results
|
||||
}()
|
||||
group.addTask { [terminal, buildArg, contextDir, label, noCache, target, quiet, cacheIn, cacheOut, pull] in
|
||||
group.addTask { [terminal, buildArg, contextDir, hiddenDockerDir, label, noCache, target, quiet, cacheIn, cacheOut, pull] in
|
||||
let config = Builder.BuildConfig(
|
||||
buildID: buildID,
|
||||
contentStore: RemoteContentStoreClient(),
|
||||
buildArgs: buildArg,
|
||||
contextDir: contextDir,
|
||||
dockerfile: buildFileData,
|
||||
hiddenDockerDir: hiddenDockerDir,
|
||||
labels: label,
|
||||
noCache: noCache,
|
||||
platforms: [Platform](platforms),
|
||||
@@ -416,8 +428,8 @@ extension Application {
|
||||
}
|
||||
}
|
||||
|
||||
public func validate() throws {
|
||||
// NOTE: We'll "validate" the Dockerfile later.
|
||||
public mutating func validate() throws {
|
||||
// NOTE: Here we check the Dockerfile exists, and set `dockerfile` to point the valid Dockerfile path or stdin
|
||||
guard FileManager.default.fileExists(atPath: contextDir) else {
|
||||
throw ValidationError("context dir does not exist \(contextDir)")
|
||||
}
|
||||
@@ -426,6 +438,31 @@ extension Application {
|
||||
throw ValidationError("invalid reference \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
switch file {
|
||||
case "-":
|
||||
dockerfile = "-"
|
||||
break
|
||||
case .some(let filepath):
|
||||
let fileURL = URL(fileURLWithPath: filepath, relativeTo: .currentDirectory())
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||
throw ValidationError("dockerfile does not exist \(filepath)")
|
||||
}
|
||||
|
||||
dockerfile = fileURL.path
|
||||
break
|
||||
case .none:
|
||||
guard let defaultDockerfile = try BuildFile.resolvePath(contextDir: contextDir) else {
|
||||
throw ValidationError("dockerfile not found in context dir")
|
||||
}
|
||||
|
||||
guard FileManager.default.fileExists(atPath: defaultDockerfile) else {
|
||||
throw ValidationError("dockerfile does not exist \(defaultDockerfile)")
|
||||
}
|
||||
|
||||
dockerfile = defaultDockerfile
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ extension TestCLIBuildBase {
|
||||
|
||||
ADD . .
|
||||
|
||||
RUN cat emptyFile
|
||||
RUN cat emptyFile
|
||||
RUN cat Test/testempty
|
||||
"""
|
||||
let context: [FileSystemEntry] = [
|
||||
@@ -154,8 +154,8 @@ extension TestCLIBuildBase {
|
||||
let tempDir: URL = try createTempDir()
|
||||
let dockerfile: String =
|
||||
"""
|
||||
ARG TAG=unknown
|
||||
FROM ghcr.io/linuxcontainers/alpine:${TAG}
|
||||
ARG TAG=unknown
|
||||
FROM ghcr.io/linuxcontainers/alpine:${TAG}
|
||||
"""
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile)
|
||||
let imageName: String = "registry.local/build-arg:\(UUID().uuidString)"
|
||||
@@ -197,7 +197,7 @@ extension TestCLIBuildBase {
|
||||
ARG TAG=3.20
|
||||
FROM ghcr.io/linuxcontainers/alpine:${TAG}
|
||||
|
||||
# stage 2 RUN
|
||||
# stage 2 RUN
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
RUN echo "Hello, World!" > /hello.txt
|
||||
|
||||
@@ -205,8 +205,8 @@ extension TestCLIBuildBase {
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
RUN ["sh", "-c", "echo 'Exec form' > /exec.txt"]
|
||||
|
||||
# stage 4 - CMD
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
# stage 4 - CMD
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
CMD ["echo", "Exec default"]
|
||||
|
||||
# stage 5 - CMD []
|
||||
@@ -297,9 +297,9 @@ extension TestCLIBuildBase {
|
||||
ADD Test1Source Test1Source
|
||||
ADD Test1Source2 Test1Source2
|
||||
|
||||
RUN cat Test1Source2/test.yaml
|
||||
RUN cat Test1Source2/test.yaml
|
||||
|
||||
# Test2: Test symlinks in nested directories
|
||||
# Test2: Test symlinks in nested directories
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
|
||||
ADD Test2Source Test2Source
|
||||
@@ -307,7 +307,7 @@ extension TestCLIBuildBase {
|
||||
|
||||
RUN cat Test2Source2/Test/test.txt
|
||||
|
||||
# Test 3: Test symlinks to directories work
|
||||
# Test 3: Test symlinks to directories work
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
|
||||
ADD Test3Source Test3Source
|
||||
@@ -371,7 +371,7 @@ extension TestCLIBuildBase {
|
||||
}
|
||||
|
||||
@Test func testBuildDifferentPaths() throws {
|
||||
let dockerfileCtxDir: URL = try createTempDir()
|
||||
let buildContextDir: URL = try createTempDir()
|
||||
let dockerfile: String =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
@@ -381,22 +381,17 @@ extension TestCLIBuildBase {
|
||||
|
||||
RUN cat /root/Test/test.txt
|
||||
"""
|
||||
let dockerfileCtx: [FileSystemEntry] = [
|
||||
let buildContext: [FileSystemEntry] = [
|
||||
.directory(".git"),
|
||||
.file(".git/FETCH", content: .zeroFilled(size: 1)),
|
||||
]
|
||||
try createContext(tempDir: dockerfileCtxDir, dockerfile: dockerfile, context: dockerfileCtx)
|
||||
|
||||
let buildContextDir: URL = try createTempDir()
|
||||
let buildContext: [FileSystemEntry] = [
|
||||
.directory("Test"),
|
||||
.file("Test/test.txt", content: .zeroFilled(size: 1)),
|
||||
]
|
||||
try createContext(tempDir: buildContextDir, dockerfile: "", context: buildContext)
|
||||
try createContext(tempDir: buildContextDir, dockerfile: dockerfile, context: buildContext)
|
||||
|
||||
let imageName = "registry.local/build-diff-context:\(UUID().uuidString)"
|
||||
#expect(throws: Never.self) {
|
||||
try self.buildWithPaths(tags: [imageName], tempContext: buildContextDir, tempDockerfileContext: dockerfileCtxDir)
|
||||
try self.build(tags: [imageName], tempDir: buildContextDir)
|
||||
}
|
||||
#expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)")
|
||||
}
|
||||
@@ -409,7 +404,7 @@ extension TestCLIBuildBase {
|
||||
|
||||
ADD . .
|
||||
|
||||
RUN cat emptyFile
|
||||
RUN cat emptyFile
|
||||
RUN cat Test/testempty
|
||||
"""
|
||||
let context: [FileSystemEntry] = [
|
||||
@@ -757,8 +752,414 @@ extension TestCLIBuildBase {
|
||||
#expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)")
|
||||
}
|
||||
|
||||
// Test 1: Basic .dockerignore
|
||||
@Test func testDockerIgnoreBasic() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file("included.txt", content: .data("This file should be included in the build context.\n".data(using: .utf8)!)),
|
||||
.file("ignored.txt", content: .data("This file should be ignored by .dockerignore.\n".data(using: .utf8)!)),
|
||||
.file(".dockerignore", content: .data("ignored.txt\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let dockerfilePath = contextDir.appendingPathComponent("Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-basic:\(UUID().uuidString)"
|
||||
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-basic-\(UUID().uuidString)"
|
||||
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 in the image")
|
||||
|
||||
let ignoredResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/ignored.txt"])
|
||||
#expect(ignoredResult.status != 0, "ignored.txt should NOT be present in the image")
|
||||
}
|
||||
|
||||
// Test 2: Dockerfile-specific ignore file (Dockerfile.dockerignore takes precedence over .dockerignore)
|
||||
@Test func testDockerIgnoreDockerfileSpecific() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// .dockerignore ignores general.txt; Dockerfile.dockerignore ignores specific.txt.
|
||||
// When both exist, Dockerfile.dockerignore takes precedence, so general.txt is included.
|
||||
// Dockerfile and its .dockerignore must be co-located; here both live in the context root.
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file(".dockerignore", content: .data("general.txt\n".data(using: .utf8)!)),
|
||||
.file("Dockerfile.dockerignore", content: .data("specific.txt\n".data(using: .utf8)!)),
|
||||
.file("general.txt", content: .data("This file should be included (Dockerfile.dockerignore takes precedence over .dockerignore).\n".data(using: .utf8)!)),
|
||||
.file("specific.txt", content: .data("This file should be ignored by Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let dockerfilePath = contextDir.appendingPathComponent("Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-specific:\(UUID().uuidString)"
|
||||
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-specific-\(UUID().uuidString)"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let specificResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/specific.txt"])
|
||||
#expect(specificResult.status != 0, "specific.txt should NOT be present (ignored by Dockerfile.dockerignore)")
|
||||
|
||||
let generalResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/general.txt"])
|
||||
#expect(generalResult.status == 0, "general.txt should be present (only in .dockerignore, not Dockerfile.dockerignore)")
|
||||
|
||||
let listResult = try run(arguments: ["exec", containerName, "ls", "-a"])
|
||||
let listFiles = listResult.output.components(separatedBy: "\n").filter { !$0.isEmpty && $0 != "." && $0 != ".." }
|
||||
#expect(Set(listFiles) == Set(["Dockerfile", ".dockerignore", "Dockerfile.dockerignore", "general.txt"]), "temporary directory must not be detected")
|
||||
}
|
||||
|
||||
@Test func testDockerIgnoreOutsideContext() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// .dockerignore ignores general.txt; Dockerfile.dockerignore ignores specific.txt.
|
||||
// When both exist, Dockerfile.dockerignore takes precedence, so general.txt is included.
|
||||
// Dockerfile and its .dockerignore must be co-located; here both live in the context root.
|
||||
let context: [FileSystemEntry] = [
|
||||
.file(".dockerignore", content: .data("general.txt\n".data(using: .utf8)!)),
|
||||
.file("general.txt", content: .data("This file should be included (Dockerfile.dockerignore takes precedence over .dockerignore).\n".data(using: .utf8)!)),
|
||||
.file("specific.txt", content: .data("This file should be ignored by Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let dockerignore = "specific.txt\n".data(using: .utf8)!
|
||||
try dockerignore.write(to: tempDir.appendingPathComponent("Dockerfile.dockerignore"), options: .atomic)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let dockerfilePath = tempDir.appendingPathComponent("Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-specific:\(UUID().uuidString)"
|
||||
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-specific-\(UUID().uuidString)"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let specificResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/specific.txt"])
|
||||
#expect(specificResult.status != 0, "specific.txt should NOT be present (ignored by Dockerfile.dockerignore)")
|
||||
|
||||
let generalResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/general.txt"])
|
||||
#expect(generalResult.status == 0, "general.txt should be present (only in .dockerignore, not Dockerfile.dockerignore)")
|
||||
}
|
||||
|
||||
// Test 5: Build succeeds when Dockerfile is listed in .dockerignore
|
||||
@Test func testDockerIgnoreIgnoredDockerfile() async throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// Dockerfile is listed in .dockerignore but build must still succeed.
|
||||
// Dockerfile lives in the context root so the ignore rule applies to it.
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file(".dockerignore", content: .data("Dockerfile\n.dockerignore\n".data(using: .utf8)!)),
|
||||
.file("test.txt", content: .data("This file should be included even though Dockerfile is ignored.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let dockerfilePath = contextDir.appendingPathComponent("Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-ignored-dockerfile:\(UUID().uuidString)"
|
||||
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-ignored-dockerfile"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let dockerfileResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/Dockerfile"])
|
||||
#expect(dockerfileResult.status != 0, "Dockerfile should NOT be present in the image")
|
||||
|
||||
let dockerignoreResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/.dockerignore"])
|
||||
#expect(dockerignoreResult.status != 0, ".dockerignore should NOT be present in the image")
|
||||
|
||||
let testFileResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/test.txt"])
|
||||
#expect(testFileResult.status == 0, "test.txt should be present in the image")
|
||||
}
|
||||
|
||||
// Test 8: Dockerfile in nested subdirectory; Dockerfile.dockerignore next to it takes precedence over root .dockerignore
|
||||
@Test func testDockerIgnoreSubdirDockerfile() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// Root .dockerignore ignores included.txt; nested Dockerfile.dockerignore ignores secret.txt
|
||||
// When Dockerfile is in nested/project/, Dockerfile.dockerignore next to it takes precedence
|
||||
let context: [FileSystemEntry] = [
|
||||
.file(".dockerignore", content: .data("included.txt\n".data(using: .utf8)!)),
|
||||
.file("included.txt", content: .data("This file should be included (Dockerfile.dockerignore takes precedence).\n".data(using: .utf8)!)),
|
||||
.file("secret.txt", content: .data("This file should be ignored by Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
.file("nested/secret.txt", content: .data("This file should be ignored by Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
.file("nested/project/Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file("nested/project/Dockerfile.dockerignore", content: .data("secret.txt\n**/secret.txt\n".data(using: .utf8)!)),
|
||||
.file("nested/project/config.txt", content: .data("This config file should be included.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let nestedDockerfile = contextDir.appendingPathComponent("nested/project/Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-subdir:\(UUID().uuidString)"
|
||||
let args = ["build", "-f", nestedDockerfile.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-subdir-\(UUID().uuidString)"
|
||||
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 (Dockerfile.dockerignore takes precedence over .dockerignore)")
|
||||
|
||||
let secretResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/secret.txt"])
|
||||
#expect(secretResult.status != 0, "secret.txt should NOT be present (ignored by Dockerfile.dockerignore)")
|
||||
|
||||
let nestedSecretResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/nested/secret.txt"])
|
||||
#expect(nestedSecretResult.status != 0, "nested/secret.txt should NOT be present (ignored by Dockerfile.dockerignore)")
|
||||
|
||||
let configResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/nested/project/config.txt"])
|
||||
#expect(configResult.status == 0, "nested/project/config.txt should be present")
|
||||
}
|
||||
|
||||
// Test 9: Custom-named Dockerfile (app1.Dockerfile) uses app1.Dockerfile.dockerignore
|
||||
@Test func testDockerIgnoreCustomDockerfileName() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// .dockerignore ignores generic.txt; app1.Dockerfile.dockerignore ignores app1-specific.txt
|
||||
// When building with -f app1.Dockerfile, app1.Dockerfile.dockerignore takes precedence
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file(".dockerignore", content: .data("generic.txt\n".data(using: .utf8)!)),
|
||||
.file("app1.Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file("app1.Dockerfile.dockerignore", content: .data("app1-specific.txt\n".data(using: .utf8)!)),
|
||||
.file("app1-specific.txt", content: .data("This file should be ignored by app1.Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
.file("generic.txt", content: .data("This file should be included (only in .dockerignore, not app1.Dockerfile.dockerignore).\n".data(using: .utf8)!)),
|
||||
.file("included.txt", content: .data("This file should always be included.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: "", context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let customDockerfile = contextDir.appendingPathComponent("app1.Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-custom-name:\(UUID().uuidString)"
|
||||
let args = ["build", "-f", customDockerfile.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-custom-name-\(UUID().uuidString)"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let app1SpecificResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/app1-specific.txt"])
|
||||
#expect(app1SpecificResult.status != 0, "app1-specific.txt should NOT be present (ignored by app1.Dockerfile.dockerignore)")
|
||||
|
||||
let genericResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/generic.txt"])
|
||||
#expect(genericResult.status == 0, "generic.txt should be present (only in .dockerignore, not app1.Dockerfile.dockerignore)")
|
||||
|
||||
let includedResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/included.txt"])
|
||||
#expect(includedResult.status == 0, "included.txt should be present")
|
||||
}
|
||||
|
||||
// Test 10: Custom-named Dockerfile in subdirectory uses its co-located .dockerignore
|
||||
@Test func testDockerIgnoreCustomNameSubdir() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
// Root .dockerignore ignores from-root-ignore.txt
|
||||
// nested/project/app2.Dockerfile.dockerignore ignores from-app2-ignore.txt
|
||||
// When building with -f nested/project/app2.Dockerfile, the nested ignore takes precedence
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file(".dockerignore", content: .data("from-root-ignore.txt\n".data(using: .utf8)!)),
|
||||
.file("from-root-ignore.txt", content: .data("This file should be included (only in .dockerignore, not app2.Dockerfile.dockerignore).\n".data(using: .utf8)!)),
|
||||
.file("from-app2-ignore.txt", content: .data("This file should be ignored by app2.Dockerfile.dockerignore.\n".data(using: .utf8)!)),
|
||||
.file("always-included.txt", content: .data("This file should always be included.\n".data(using: .utf8)!)),
|
||||
.file("nested/project/app2.Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
|
||||
.file("nested/project/app2.Dockerfile.dockerignore", content: .data("from-app2-ignore.txt\n".data(using: .utf8)!)),
|
||||
.file("nested/project/config.yaml", content: .data("Config file in project directory.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: "", context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let customDockerfile = contextDir.appendingPathComponent("nested/project/app2.Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-custom-subdir:\(UUID().uuidString)"
|
||||
let args = ["build", "-f", customDockerfile.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-custom-subdir-\(UUID().uuidString)"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let app2IgnoreResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/from-app2-ignore.txt"])
|
||||
#expect(app2IgnoreResult.status != 0, "from-app2-ignore.txt should NOT be present (ignored by app2.Dockerfile.dockerignore)")
|
||||
|
||||
let rootIgnoreResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/from-root-ignore.txt"])
|
||||
#expect(rootIgnoreResult.status == 0, "from-root-ignore.txt should be present (only in .dockerignore, not app2.Dockerfile.dockerignore)")
|
||||
|
||||
let alwaysIncludedResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/always-included.txt"])
|
||||
#expect(alwaysIncludedResult.status == 0, "always-included.txt should be present")
|
||||
|
||||
let configResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/nested/project/config.yaml"])
|
||||
#expect(configResult.status == 0, "nested/project/config.yaml should be present")
|
||||
}
|
||||
|
||||
// Test 11: app.Dockerfile coexists with Dockerfile; app.Dockerfile.dockerignore is used, not Dockerfile.dockerignore
|
||||
@Test func testDockerIgnoreCoexistingDockerfiles() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let appDockerfile =
|
||||
"""
|
||||
FROM ghcr.io/linuxcontainers/alpine:3.20
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
"""
|
||||
let context: [FileSystemEntry] = [
|
||||
.file("Dockerfile", content: .data("FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . .\n".data(using: .utf8)!)),
|
||||
.file("Dockerfile.dockerignore", content: .data("dockerfile-specific.txt\n".data(using: .utf8)!)),
|
||||
.file("app.Dockerfile", content: .data(appDockerfile.data(using: .utf8)!)),
|
||||
.file("app.Dockerfile.dockerignore", content: .data("app-specific.txt\n".data(using: .utf8)!)),
|
||||
.file(
|
||||
"dockerfile-specific.txt", content: .data("This file should NOT be copied when using Dockerfile, but SHOULD when using app.Dockerfile.\n".data(using: .utf8)!)),
|
||||
.file("app-specific.txt", content: .data("This file should NOT be copied (ignored by app.Dockerfile.dockerignore).\n".data(using: .utf8)!)),
|
||||
.file("included.txt", content: .data("This file should be copied.\n".data(using: .utf8)!)),
|
||||
]
|
||||
try createContext(tempDir: tempDir, dockerfile: "", context: context)
|
||||
|
||||
let contextDir = tempDir.appendingPathComponent("context")
|
||||
let appDockerfilePath = contextDir.appendingPathComponent("app.Dockerfile")
|
||||
let imageName = "registry.local/dockerignore-coexisting:\(UUID().uuidString)"
|
||||
let args = ["build", "-f", appDockerfilePath.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-coexisting-\(UUID().uuidString)"
|
||||
try self.doLongRun(name: containerName, image: imageName)
|
||||
defer { try? self.doStop(name: containerName) }
|
||||
|
||||
let appSpecificResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/app-specific.txt"])
|
||||
#expect(appSpecificResult.status != 0, "app-specific.txt should NOT be present (ignored by app.Dockerfile.dockerignore)")
|
||||
|
||||
let dockerfileSpecificResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/dockerfile-specific.txt"])
|
||||
#expect(dockerfileSpecificResult.status == 0, "dockerfile-specific.txt should be present (Dockerfile.dockerignore was not used)")
|
||||
|
||||
let includedResult = try run(arguments: ["exec", containerName, "test", "-f", "/app/included.txt"])
|
||||
#expect(includedResult.status == 0, "included.txt should be present")
|
||||
}
|
||||
|
||||
@Test func testNonExistingDockerfile() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let imageName = "registry.local/non-existing-dockerfile:\(UUID().uuidString)"
|
||||
|
||||
var args = ["build", "-f", "non-existing-path", "-t", imageName, tempDir.path]
|
||||
var response = try run(arguments: args)
|
||||
|
||||
#expect(response.status != 0)
|
||||
|
||||
args = ["build", "-t", imageName, tempDir.path]
|
||||
response = try run(arguments: args)
|
||||
|
||||
#expect(response.status != 0)
|
||||
}
|
||||
|
||||
@Test func testBuildNoCachePullLatestImage() throws {
|
||||
let tempDir: URL = try createTempDir()
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let dockerfile =
|
||||
"""
|
||||
FROM \(alpine)
|
||||
|
||||
Reference in New Issue
Block a user