Files
2026-06-30 10:40:26 -07:00

1105 lines
53 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Darwin
import Foundation
import Testing
// Convenience alias for the verbose entry type.
typealias FSEntry = ContainerFixture.FileSystemEntry
@Suite(.serialized)
struct TestCLIBuilderSerial {
// MARK: - Basic build tests
@Test func testBuildDefaultParams() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(dir: dir, dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20")
// No tags — runtime generates one and prints it to stdout.
let output = try f.buildWithPaths(contextDir: dir)
let generatedTag = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(!generatedTag.isEmpty, "build should print the generated image tag to stdout")
try f.assertImageBuilt(generatedTag)
}
}
}
@Test func testBuildDotFileSucceeds() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM scratch\nADD emptyFile /",
context: [
.file("emptyFile", content: .zeroFilled(size: 1)),
.file(".dockerignore", content: .data(".dockerignore\n".data(using: .utf8)!)),
])
let image = "registry.local/dot-file:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildFromPreviousStage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20 AS layer1
RUN sh -c "echo 'layer1' > /layer1.txt"
FROM layer1
CMD ["cat", "/layer1.txt"]
""")
let image = "registry.local/from-previous-layer:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildFromLocalImage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM scratch\nADD emptyFile /",
context: [
.file("emptyFile", content: .zeroFilled(size: 0)),
.file(".dockerignore", content: .data(".dockerignore\n".data(using: .utf8)!)),
])
let image = "local-only:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
let dir2 = try f.createTempDir()
try f.createContext(
dir: dir2,
dockerfile: "FROM \(image)",
context: [])
let image2 = "from-local:\(UUID().uuidString)"
try f.build(tag: image2, contextDir: dir2)
try f.assertImageBuilt(image2)
}
}
}
@Test func testBuildAddFromSpecialDirs() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { 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 image = "registry.local/scratch-add-special-dir:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildScratchAdd() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { 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 image = "registry.local/scratch-add:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildAddAll() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD . .
RUN cat emptyFile
RUN cat Test/testempty
""",
context: [
.directory("Test"),
.file("Test/testempty", content: .zeroFilled(size: 1)),
.file("emptyFile", content: .zeroFilled(size: 1)),
])
let image = "registry.local/add-all:\(UUID().uuidString)"
let output = try f.build(tag: image, contextDir: dir)
#expect(output.contains(image))
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "ARG TAG=unknown\nFROM ghcr.io/linuxcontainers/alpine:${TAG}")
let image = "registry.local/build-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, buildArgs: ["TAG=3.20"])
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildSecret() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN --mount=type=secret,id=ENV1 \\
--mount=type=secret,id=env2 \\
--mount=type=secret,id=env3 \\
test xyyzzz = "`cat /run/secrets/ENV1 /run/secrets/env2 /run/secrets/env3`"
RUN --mount=type=secret,id=file \\
awk 'BEGIN {for(i=0; i<17; i++) for(c=0; c<256; c++) printf("%c", c)}' > /tmp/foo && \\
cmp /tmp/foo /run/secrets/file && \\
rm /tmp/foo
RUN --mount=type=secret,id=empty \\
! test -e /run/secrets/file && \\
test -e /run/secrets/empty && \\
cmp /dev/null /run/secrets/empty
""")
setenv("ENV1", "x", 1)
setenv("ENV_VAR", "yy", 1)
setenv("env3", "zzz", 1)
f.addCleanup {
unsetenv("ENV1")
unsetenv("ENV_VAR")
unsetenv("env3")
}
let testData = Data((0..<17).flatMap { _ in Array(0...255) })
let secretFile = try f.createTempFile(suffix: " _f,i=l.e+ ", contents: testData)
let emptyFile = try f.createTempFile(suffix: "file2", contents: Data())
let image = "registry.local/secrets:\(UUID().uuidString)"
try f.build(
tag: image, contextDir: dir,
otherArgs: [
"--secret", "id=ENV1",
"--secret", "id=env2,env=ENV_VAR",
"--secret", "id=env3,env=env3",
"--secret", "id=file,src=\(secretFile.string)",
"--secret", "id=empty,src=\(emptyFile.string)",
])
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildNetworkAccess() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ARG http_proxy
ARG https_proxy
ARG no_proxy
RUN apk add --no-cache curl
""")
var buildArgs: [String] = []
for key in ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"] {
if let v = ProcessInfo.processInfo.environment[key] { buildArgs.append("\(key)=\(v)") }
}
let image = "registry.local/build-network-access:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, buildArgs: buildArgs)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildDockerfileKeywords() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
ARG TAG=3.20
FROM ghcr.io/linuxcontainers/alpine:${TAG}
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN echo "Hello, World!" > /hello.txt
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN ["sh", "-c", "echo 'Exec form' > /exec.txt"]
FROM ghcr.io/linuxcontainers/alpine:3.20
CMD ["echo", "Exec default"]
FROM ghcr.io/linuxcontainers/alpine:3.20
LABEL version="1.0" description="Test image"
FROM ghcr.io/linuxcontainers/alpine:3.20
EXPOSE 8080
FROM ghcr.io/linuxcontainers/alpine:3.20
ENV MY_ENV=hello
RUN echo $MY_ENV > /env.txt
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD emptyFile /
FROM ghcr.io/linuxcontainers/alpine:3.20
COPY toCopy /toCopy
FROM ghcr.io/linuxcontainers/alpine:3.20
ENTRYPOINT ["echo", "entrypoint!"]
FROM ghcr.io/linuxcontainers/alpine:3.20
VOLUME /data
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN adduser -D myuser
USER myuser
CMD whoami
FROM ghcr.io/linuxcontainers/alpine:3.20
WORKDIR /app
RUN pwd > /pwd.out
FROM ghcr.io/linuxcontainers/alpine:3.20
ARG MY_VAR=default
RUN echo $MY_VAR > /var.out
""",
context: [
.file("emptyFile", content: .zeroFilled(size: 1)),
.file("toCopy", content: .zeroFilled(size: 1)),
])
let image = "registry.local/dockerfile-keywords:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildSymlink() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = """
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD Test1Source Test1Source
ADD Test1Source2 Test1Source2
RUN cat Test1Source2/test.yaml
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD Test2Source Test2Source
ADD Test2Source2 Test2Source2
RUN cat Test2Source2/Test/test.txt
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD Test3Source Test3Source
ADD Test3Source2 Test3Source2
RUN cat Test3Source2/Dest/test.txt
"""
let context: [FSEntry] = [
.directory("Test1Source"), .directory("Test1Source2"),
.file("Test1Source/test.yaml", content: .zeroFilled(size: 200)),
.symbolicLink("Test1Source2/test.yaml", target: "Test1Source/test.yaml"),
.directory("Test2Source"), .directory("Test2Source2"),
.file("Test2Source/Test/Test/test.yaml", content: .zeroFilled(size: 300)),
.symbolicLink("Test2Source2/Test/test.yaml", target: "Test2Source/Test/Test/test.yaml"),
.directory("Test3Source/Source"), .directory("Test3Source2"),
.file("Test3Source/Source/test.txt", content: .zeroFilled(size: 1)),
.symbolicLink("Test3Source2/Dest", target: "Test3Source/Source"),
]
try f.createContext(dir: dir, dockerfile: dockerfile, context: context)
let image = "registry.local/build-symlinks:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildAndRun() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nRUN echo \"foobar\" > /file")
let image = "\(f.testID)-build-and-run:latest"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
try await f.withContainer(image: image) { name in
let output = try f.doExec(name, cmd: ["cat", "/file"])
.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == "foobar")
}
}
}
}
@Test func testBuildDifferentPaths() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN ls ./
COPY . /root
RUN cat /root/Test/test.txt
""",
context: [
.directory(".git"),
.file(".git/FETCH", content: .zeroFilled(size: 1)),
.directory("Test"),
.file("Test/test.txt", content: .zeroFilled(size: 1)),
])
let image = "registry.local/build-diff-context:\(UUID().uuidString)"
try f.buildWithPaths(tags: [image], contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildMultiArch() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
ADD . .
RUN cat emptyFile
RUN cat Test/testempty
""",
context: [
.directory("Test"),
.file("Test/testempty", content: .zeroFilled(size: 1)),
.file("emptyFile", content: .zeroFilled(size: 1)),
])
let image = "registry.local/multi-arch:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, otherArgs: ["--arch", "amd64,arm64"])
try f.assertImageBuilt(image)
let output = try f.doInspectImages(image)
#expect(output.count == 1, "expected single inspect result")
let archs = Set(output[0].variants.map { $0.platform.architecture })
#expect(archs == Set(["amd64", "arm64"]), "expected amd64 and arm64 variants")
}
}
}
@Test func testBuildMultipleTags() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { 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 uuid = UUID().uuidString
let tag1 = "registry.local/multi-tag-test:\(uuid)"
let tag2 = "registry.local/multi-tag-test:latest"
let tag3 = "registry.local/multi-tag-test:v1.0.0"
let output = try f.buildWithPaths(tags: [tag1, tag2, tag3], contextDir: dir)
#expect(output.contains(tag1))
#expect(output.contains(tag2))
#expect(output.contains(tag3))
try f.assertImageBuilt(tag1)
try f.assertImageBuilt(tag2)
try f.assertImageBuilt(tag3)
}
}
}
@Test func testBuildAfterContextChange() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let initialContent = "initial".data(using: .utf8)!
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY foo /foo\nCOPY bar /bar",
context: [
.file("foo", content: .data(Data((0..<4 * 1024 * 1024).map { UInt8($0 % 256) }))),
.file("bar", content: .data(initialContent)),
])
let image1 = "\(f.testID)-build-context-change:v1"
try f.build(tag: image1, contextDir: dir)
try await f.withContainer(image: image1) { name in
let out = try f.doExec(name, cmd: ["cat", "/bar"])
#expect(out == "initial")
}
let contextBar = dir.appending("context").appending("bar")
try "updated".data(using: .utf8)!.write(to: URL(filePath: contextBar.string), options: .atomic)
let image2 = "\(f.testID)-build-context-change:v2"
try f.build(tag: image2, contextDir: dir)
try await f.withContainer(image: image2) { name in
let out = try f.doExec(name, cmd: ["cat", "/bar"])
#expect(out == "updated")
}
}
}
}
@Test func testBuildWithDockerfileFromStdin() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM scratch\nADD emptyFile /"
try f.createContext(
dir: dir, dockerfile: "",
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
let image = "registry.local/stdin-file:\(UUID().uuidString)"
try f.buildWithStdin(tags: [image], contextDir: dir, dockerfileContents: dockerfile)
try f.assertImageBuilt(image)
}
}
}
@Test func testLowercaseDockerfile() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let files: [(String, String, String)] = [
("COPY . /app", "copy-uppercase", "COPY"),
("copy . /app", "copy-lowercase", "copy"),
("ADD . /app", "add-uppercase", "ADD"),
("add . /app", "add-lowercase", "add"),
]
for (instruction, name, _) in files {
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
\(instruction)
RUN test -f /app/testfile.txt
""",
context: [.file("testfile.txt", content: .data("test".data(using: .utf8)!))])
let image = "registry.local/\(name):\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
}
@Test func testRunWithBindMount() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
RUN --mount=type=bind,source=.,target=/mnt/context \\
set -e; \\
if [ ! -f /mnt/context/app.py ]; then echo "ERROR: app.py missing"; exit 1; fi; \\
if [ ! -f /mnt/context/config.yaml ]; then echo "ERROR: config.yaml missing"; exit 1; fi; \\
cp /mnt/context/app.py /app.py
RUN cat /app.py
""",
context: [
.file("app.py", content: .data("print('Hello from bind mount')".data(using: .utf8)!)),
.file("config.yaml", content: .data("key: value".data(using: .utf8)!)),
])
let image = "registry.local/bind-mount-test:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
// MARK: - .dockerignore tests
@Test func testBuildDockerIgnore() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerignore = """
secret.txt
*.log
**/*.log
!important.log
*.tmp
**/*.tmp
temp/
node_modules/
"""
try f.createContext(
dir: dir,
dockerfile: """
FROM ghcr.io/linuxcontainers/alpine:3.20
COPY . /app
RUN set -e; [ ! -f /app/secret.txt ] || exit 1
RUN set -e; [ ! -f /app/debug.log ] || exit 1
RUN set -e; [ -f /app/important.log ] || exit 1
RUN set -e; find /app -name "*.tmp" | grep . && exit 1; true
RUN set -e; [ ! -d /app/temp ] || exit 1
RUN set -e; [ ! -d /app/node_modules ] || exit 1
RUN set -e; [ -f /app/main.go ] && [ -f /app/README.md ] && [ -f /app/src/app.go ]
""",
context: [
.file(".dockerignore", content: .data(dockerignore.data(using: .utf8)!)),
.file("secret.txt", content: .data("secret".data(using: .utf8)!)),
.file("debug.log", content: .data("debug".data(using: .utf8)!)),
.file("important.log", content: .data("important".data(using: .utf8)!)),
.file("cache.tmp", content: .data("cache".data(using: .utf8)!)),
.file("main.go", content: .data("package main".data(using: .utf8)!)),
.file("README.md", content: .data("# README".data(using: .utf8)!)),
.directory("temp"),
.file("logs/app.log", content: .data("app log".data(using: .utf8)!)),
.directory("node_modules"),
.directory("src"),
.file("src/app.go", content: .data("package src".data(using: .utf8)!)),
])
let image = "registry.local/dockerignore-test:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testDockerIgnoreBasic() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir,
dockerfile: dockerfile,
context: [
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
.file("included.txt", content: .data("included\n".data(using: .utf8)!)),
.file("ignored.txt", content: .data("ignored\n".data(using: .utf8)!)),
.file(".dockerignore", content: .data("ignored.txt\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let image = "registry.local/dockerignore-basic:\(UUID().uuidString)"
let result = try f.run([
"build", "-f", contextDir.appending("Dockerfile").string,
"-t", image, contextDir.string,
])
try result.check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerHasFile(name, at: "/app/included.txt")
try f.assertContainerMissingFile(name, at: "/app/ignored.txt")
}
}
}
}
@Test func testDockerIgnoreDockerfileSpecific() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: dockerfile,
context: [
.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("general\n".data(using: .utf8)!)),
.file("specific.txt", content: .data("specific\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let image = "registry.local/dockerignore-specific:\(UUID().uuidString)"
try f.run([
"build", "-f", contextDir.appending("Dockerfile").string,
"-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/specific.txt", "specific.txt should be ignored by Dockerfile.dockerignore")
try f.assertContainerHasFile(name, at: "/app/general.txt", "general.txt should be present (Dockerfile.dockerignore takes precedence)")
}
}
}
}
@Test func testDockerIgnoreOutsideContext() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: dockerfile,
context: [
.file(".dockerignore", content: .data("general.txt\n".data(using: .utf8)!)),
.file("general.txt", content: .data("general\n".data(using: .utf8)!)),
.file("specific.txt", content: .data("specific\n".data(using: .utf8)!)),
])
try "specific.txt\n".data(using: .utf8)!.write(to: URL(filePath: dir.appending("Dockerfile.dockerignore").string), options: .atomic)
let image = "registry.local/dockerignore-outside:\(UUID().uuidString)"
try f.run([
"build", "-f", dir.appending("Dockerfile").string,
"-t", image, dir.appending("context").string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/specific.txt")
try f.assertContainerHasFile(name, at: "/app/general.txt")
}
}
}
}
@Test func testDockerIgnoreIgnoredDockerfile() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: dockerfile,
context: [
.file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
.file(".dockerignore", content: .data("Dockerfile\n.dockerignore\n".data(using: .utf8)!)),
.file("test.txt", content: .data("test\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let image = "registry.local/dockerignore-ignored-dockerfile:\(UUID().uuidString)"
try f.run([
"build", "-f", contextDir.appending("Dockerfile").string,
"-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/Dockerfile")
try f.assertContainerMissingFile(name, at: "/app/.dockerignore")
try f.assertContainerHasFile(name, at: "/app/test.txt")
}
}
}
}
@Test func testDockerIgnoreSubdirDockerfile() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: dockerfile,
context: [
.file(".dockerignore", content: .data("included.txt\n".data(using: .utf8)!)),
.file("included.txt", content: .data("included\n".data(using: .utf8)!)),
.file("secret.txt", content: .data("secret\n".data(using: .utf8)!)),
.file("nested/secret.txt", content: .data("nested secret\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("config\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let nestedDockerfile = contextDir.appending("nested").appending("project").appending("Dockerfile")
let image = "registry.local/dockerignore-subdir:\(UUID().uuidString)"
try f.run([
"build", "-f", nestedDockerfile.string, "-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerHasFile(name, at: "/app/included.txt")
try f.assertContainerMissingFile(name, at: "/app/secret.txt")
try f.assertContainerMissingFile(name, at: "/app/nested/secret.txt")
try f.assertContainerHasFile(name, at: "/app/nested/project/config.txt")
}
}
}
}
@Test func testDockerIgnoreCustomDockerfileName() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: "", // no top-level Dockerfile
context: [
.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("app1 specific\n".data(using: .utf8)!)),
.file("generic.txt", content: .data("generic\n".data(using: .utf8)!)),
.file("included.txt", content: .data("included\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let image = "registry.local/dockerignore-custom-name:\(UUID().uuidString)"
try f.run([
"build", "-f", contextDir.appending("app1.Dockerfile").string,
"-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/app1-specific.txt")
try f.assertContainerHasFile(name, at: "/app/generic.txt")
try f.assertContainerHasFile(name, at: "/app/included.txt")
}
}
}
}
@Test func testDockerIgnoreCustomNameSubdir() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: "",
context: [
.file(".dockerignore", content: .data("from-root-ignore.txt\n".data(using: .utf8)!)),
.file("from-root-ignore.txt", content: .data("root ignore\n".data(using: .utf8)!)),
.file("from-app2-ignore.txt", content: .data("app2 ignore\n".data(using: .utf8)!)),
.file("always-included.txt", content: .data("always\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\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let nestedDockerfile = contextDir.appending("nested").appending("project").appending("app2.Dockerfile")
let image = "registry.local/dockerignore-custom-subdir:\(UUID().uuidString)"
try f.run([
"build", "-f", nestedDockerfile.string, "-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/from-app2-ignore.txt")
try f.assertContainerHasFile(name, at: "/app/from-root-ignore.txt")
try f.assertContainerHasFile(name, at: "/app/always-included.txt")
try f.assertContainerHasFile(name, at: "/app/nested/project/config.yaml")
}
}
}
}
@Test func testDockerIgnoreCoexistingDockerfiles() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let appDockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: "",
context: [
.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("df specific\n".data(using: .utf8)!)),
.file("app-specific.txt", content: .data("app specific\n".data(using: .utf8)!)),
.file("included.txt", content: .data("included\n".data(using: .utf8)!)),
])
let contextDir = dir.appending("context")
let image = "registry.local/dockerignore-coexisting:\(UUID().uuidString)"
try f.run([
"build", "-f", contextDir.appending("app.Dockerfile").string,
"-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerMissingFile(name, at: "/app/app-specific.txt")
try f.assertContainerHasFile(name, at: "/app/dockerfile-specific.txt")
try f.assertContainerHasFile(name, at: "/app/included.txt")
}
}
}
}
@Test func testDockerIgnoreReadonlyContext() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
try f.createContext(
dir: dir, dockerfile: dockerfile,
context: [
.file("included.txt", content: .data("included\n".data(using: .utf8)!)),
.file("secret.txt", content: .data("secret\n".data(using: .utf8)!)),
])
try "secret.txt\n".data(using: .utf8)!.write(to: URL(filePath: dir.appending("Dockerfile.dockerignore").string), options: .atomic)
let contextDir = dir.appending("context")
// Make the context read-only, then restore before cleanup.
try FileManager.default.setAttributes(
[.posixPermissions: 0o555], ofItemAtPath: contextDir.string)
f.addCleanup {
try? FileManager.default.setAttributes(
[.posixPermissions: 0o755], ofItemAtPath: contextDir.string)
}
let image = "registry.local/dockerignore-readonly:\(UUID().uuidString.prefix(6))"
try f.run([
"build", "-f", dir.appending("Dockerfile").string,
"-t", image, contextDir.string,
]).check()
try await f.withContainer(image: image, tag: "c") { name in
try f.assertContainerHasFile(name, at: "/app/included.txt")
try f.assertContainerMissingFile(name, at: "/app/secret.txt")
}
}
}
}
@Test func testNonExistingDockerfile() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
let image = "registry.local/non-existing-dockerfile:\(UUID().uuidString)"
let r1 = try f.run(["build", "-f", "non-existing-path", "-t", image, dir.string])
#expect(r1.status != 0)
let r2 = try f.run(["build", "-t", image, dir.string])
#expect(r2.status != 0)
}
}
}
@Test func testBuildNoCachePullLatestImage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM \(ContainerFixture.warmupImages[0])\nADD emptyFile /",
context: [.file("emptyFile", content: .zeroFilled(size: 1))])
let image = "registry.local/no-cache-pull:\(UUID().uuidString)"
try f.buildWithPaths(tags: [image], contextDir: dir, otherArgs: ["--pull", "--no-cache"])
try f.assertImageBuilt(image)
}
}
}
// MARK: - Dockerfile ARG quoting
@Test func testBuildQuotedImageDockerfileArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "ARG IMAGE=\"ghcr.io/linuxcontainers/alpine:3.20\"\nFROM $IMAGE\nRUN test -f /etc/alpine-release")
let image = "registry.local/quoted-image-dockerfile-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildQuotedStringDockerfileArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nARG MYSTRING='\"Hello, world!\"'\nRUN test \"$MYSTRING\" = '\"Hello, world!\"'")
let image = "registry.local/quoted-string-dockerfile-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildForwardReferencedDockerfileArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
ARG ALPINE="ghcr.io/linuxcontainers/alpine"
ARG IMAGE="${ALPINE}:3.20"
FROM $IMAGE
RUN test -f /etc/alpine-release
""")
let image = "registry.local/forward-referenced-dockerfile-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildQuotedImageBuildArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "ARG IMAGE\nFROM $IMAGE\nRUN test -f /etc/alpine-release")
let image = "registry.local/quoted-image-build-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, buildArgs: ["IMAGE=ghcr.io/linuxcontainers/alpine:3.20"])
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildQuotedStringBuildArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nARG MYSTRING\nRUN test \"$MYSTRING\" = '\"Hello, world!\"'")
let image = "registry.local/quoted-string-build-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, buildArgs: ["MYSTRING=\"Hello, world!\""])
try f.assertImageBuilt(image)
}
}
}
@Test func testBuildForwardReferencedBuildArg() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
ARG ALPINE
ARG IMAGE="$ALPINE:3.20"
FROM $IMAGE
RUN test -f /etc/alpine-release
""")
let image = "registry.local/forward-referenced-build-arg:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir, buildArgs: ["ALPINE=ghcr.io/linuxcontainers/alpine"])
try f.assertImageBuilt(image)
}
}
}
// MARK: - COPY --from tests
@Test func testCopyFromLocalImage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let baseDir = try f.createTempDir()
let baseName = "local-base:\(UUID().uuidString)"
try f.createContext(
dir: baseDir,
dockerfile: "FROM scratch\nADD hello.txt /hello.txt",
context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
try f.build(tag: baseName, contextDir: baseDir)
try f.assertImageBuilt(baseName)
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=\(baseName) /hello.txt /copied.txt\nRUN cat /copied.txt")
let image = "registry.local/copy-from-local:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testCopyFromBuildStage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM scratch AS builder
ADD hello.txt /hello.txt
FROM ghcr.io/linuxcontainers/alpine:3.20
COPY --from=builder /hello.txt /copied.txt
RUN cat /copied.txt
""",
context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
let image = "registry.local/copy-from-stage:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testCopyRenameFromStage() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM scratch AS builder
ADD hello.txt /hello.txt
FROM ghcr.io/linuxcontainers/alpine:3.20
COPY --from=builder /hello.txt /renamed.txt
RUN cat /renamed.txt
""",
context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
let image = "registry.local/copy-rename:\(UUID().uuidString)"
try f.build(tag: image, contextDir: dir)
try f.assertImageBuilt(image)
}
}
}
@Test func testCopyMissingFileFails() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: """
FROM scratch AS builder
FROM ghcr.io/linuxcontainers/alpine:3.20
COPY --from=builder /does-not-exist.txt /copied.txt
""")
let image = "registry.local/copy-missing:\(UUID().uuidString)"
let result = try f.run([
"build", "-f", dir.appending("Dockerfile").string,
"-t", image, dir.appending("context").string,
])
#expect(result.status != 0, "build should fail when source file is missing")
}
}
}
@Test func testCopyInvalidStageFails() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=not_a_stage /hello.txt /copied.txt")
let image = "registry.local/copy-invalid-stage:\(UUID().uuidString)"
let result = try f.run([
"build", "-f", dir.appending("Dockerfile").string,
"-t", image, dir.appending("context").string,
])
#expect(result.status != 0, "build should fail with invalid stage name")
}
}
}
@Test func testCopyFromNonexistentImageFails() async throws {
try await ContainerFixture.with { f in
try await f.withBuilder { f in
let dir = try f.createTempDir()
try f.createContext(
dir: dir,
dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=doesnotexist:latest /hello.txt /copied.txt")
let image = "registry.local/copy-bad-image:\(UUID().uuidString)"
let result = try f.run([
"build", "-f", dir.appending("Dockerfile").string,
"-t", image, dir.appending("context").string,
])
#expect(result.status != 0, "build should fail when source image does not exist")
}
}
}
}