mirror of
https://github.com/apple/container.git
synced 2026-07-14 05:27:02 +00:00
586fa07d2a
- Part of #1833.
1105 lines
53 KiB
Swift
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")
|
|
}
|
|
}
|
|
}
|
|
}
|