mirror of
https://github.com/apple/container.git
synced 2026-07-16 22:47:09 +00:00
265 lines
12 KiB
Swift
265 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
|
//
|
|
// 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 Foundation
|
|
import Testing
|
|
|
|
extension TestCLIBuildBase {
|
|
class CLIBuilderLocalOutputTest: TestCLIBuildBase {
|
|
override init() throws {
|
|
try super.init()
|
|
}
|
|
|
|
deinit {
|
|
try? builderDelete(force: true)
|
|
}
|
|
|
|
@Test func testBuildLocalOutputHappyPath() throws {
|
|
let tempDir: URL = try createTempDir()
|
|
|
|
// Test comprehensive multi-stage build with context and build arguments
|
|
let dockerfile: String =
|
|
"""
|
|
ARG MESSAGE=default
|
|
FROM scratch AS builder
|
|
ADD build.txt /build.txt
|
|
ADD testfile.txt /hello.txt
|
|
|
|
FROM scratch
|
|
COPY --from=builder /build.txt /final.txt
|
|
COPY --from=builder /hello.txt /app/hello.txt
|
|
ADD message.txt /message.txt
|
|
"""
|
|
let context: [FileSystemEntry] = [
|
|
.file("build.txt", content: .data("Building stage\n".data(using: .utf8)!)),
|
|
.file("testfile.txt", content: .data("Hello from local build\n".data(using: .utf8)!)),
|
|
.file("message.txt", content: .data("Hello from build args\n".data(using: .utf8)!)),
|
|
]
|
|
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
|
|
|
let outputDir = tempDir.appendingPathComponent("comprehensive-local-output")
|
|
let imageName = "local-comprehensive-test:\(UUID().uuidString)"
|
|
|
|
let response = try buildWithLocalOutput(
|
|
tag: imageName,
|
|
tempDir: tempDir,
|
|
outputDir: outputDir,
|
|
args: ["MESSAGE=Hello from build args"]
|
|
)
|
|
|
|
// Verify the build succeeded
|
|
#expect(response.contains("Successfully exported to"), "Expected successful local export message")
|
|
|
|
// Verify the output directory was created
|
|
#expect(FileManager.default.fileExists(atPath: outputDir.path), "Expected local output directory to exist")
|
|
|
|
// Verify the output contains expected structure
|
|
let contents = try FileManager.default.contentsOfDirectory(atPath: outputDir.path)
|
|
#expect(!contents.isEmpty, "Expected local output directory to contain files")
|
|
|
|
// Test basic functionality - verify basic local output works
|
|
let basicTempDir: URL = try createTempDir()
|
|
let basicDockerfile: String =
|
|
"""
|
|
FROM scratch
|
|
|
|
ADD testfile.txt /hello.txt
|
|
"""
|
|
let basicContext: [FileSystemEntry] = [
|
|
.file("testfile.txt", content: .data("Hello from basic build\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: basicTempDir, dockerfile: basicDockerfile, context: basicContext)
|
|
|
|
let basicOutputDir = basicTempDir.appendingPathComponent("basic-local-output")
|
|
let basicImageName = "local-basic-test:\(UUID().uuidString)"
|
|
|
|
let basicResponse = try buildWithLocalOutput(tag: basicImageName, tempDir: basicTempDir, outputDir: basicOutputDir)
|
|
|
|
// Verify basic build succeeded
|
|
#expect(basicResponse.contains("Successfully exported to"), "Expected successful basic local export message")
|
|
#expect(FileManager.default.fileExists(atPath: basicOutputDir.path), "Expected basic local output directory to exist")
|
|
|
|
// Test context functionality - verify COPY works with context
|
|
let contextTempDir: URL = try createTempDir()
|
|
let contextDockerfile: String =
|
|
"""
|
|
FROM scratch
|
|
|
|
COPY testfile.txt /app/testfile.txt
|
|
"""
|
|
let contextContext: [FileSystemEntry] = [
|
|
.file("testfile.txt", content: .data("Test content for context build\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: contextTempDir, dockerfile: contextDockerfile, context: contextContext)
|
|
|
|
let contextOutputDir = contextTempDir.appendingPathComponent("context-local-output")
|
|
let contextImageName = "local-context-test:\(UUID().uuidString)"
|
|
|
|
let contextResponse = try buildWithLocalOutput(tag: contextImageName, tempDir: contextTempDir, outputDir: contextOutputDir)
|
|
|
|
// Verify context build succeeded
|
|
#expect(contextResponse.contains("Successfully exported to"), "Expected successful context local export message")
|
|
#expect(FileManager.default.fileExists(atPath: contextOutputDir.path), "Expected context local output directory to exist")
|
|
}
|
|
|
|
@Test func testBuildLocalOutputEdgeCases() throws {
|
|
// Test building with different context paths
|
|
let dockerfileCtxDir: URL = try createTempDir()
|
|
let dockerfile: String =
|
|
"""
|
|
FROM scratch
|
|
|
|
COPY . /app
|
|
"""
|
|
let dockerfileCtx: [FileSystemEntry] = [
|
|
.file("dockerfile-context.txt", content: .data("Dockerfile context file\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: dockerfileCtxDir, dockerfile: dockerfile, context: dockerfileCtx)
|
|
|
|
let buildContextDir: URL = try createTempDir()
|
|
let buildContext: [FileSystemEntry] = [
|
|
.file("build-context.txt", content: .data("Build context file\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: buildContextDir, dockerfile: "", context: buildContext)
|
|
|
|
let outputDir = dockerfileCtxDir.appendingPathComponent("diffpaths-local-output")
|
|
let imageName = "local-diffpaths-test:\(UUID().uuidString)"
|
|
|
|
let response = try buildWithPathsAndLocalOutput(
|
|
tag: imageName,
|
|
tempContext: buildContextDir,
|
|
tempDockerfileContext: dockerfileCtxDir,
|
|
outputDir: outputDir
|
|
)
|
|
|
|
// Verify the build succeeded
|
|
#expect(response.contains("Successfully exported to"), "Expected successful local export message")
|
|
|
|
// Verify the output directory exists
|
|
#expect(FileManager.default.fileExists(atPath: outputDir.path), "Expected local output directory to exist")
|
|
|
|
// Test building to existing output directory
|
|
let existingTempDir: URL = try createTempDir()
|
|
let existingDockerfile: String =
|
|
"""
|
|
FROM scratch
|
|
|
|
ADD newfile.txt /newfile.txt
|
|
"""
|
|
let existingContext: [FileSystemEntry] = [
|
|
.file("newfile.txt", content: .data("New content from build\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: existingTempDir, dockerfile: existingDockerfile, context: existingContext)
|
|
|
|
let existingOutputDir = existingTempDir.appendingPathComponent("existing-output")
|
|
|
|
// Create the output directory and add some existing files
|
|
try FileManager.default.createDirectory(at: existingOutputDir, withIntermediateDirectories: true)
|
|
let existingFile = existingOutputDir.appendingPathComponent("existing.txt")
|
|
try "Existing file content\n".data(using: .utf8)!.write(to: existingFile)
|
|
|
|
let existingImageName = "local-existing-test:\(UUID().uuidString)"
|
|
|
|
let existingResponse = try buildWithLocalOutput(tag: existingImageName, tempDir: existingTempDir, outputDir: existingOutputDir)
|
|
|
|
// Verify the build succeeded
|
|
#expect(existingResponse.contains("Successfully exported to"), "Expected successful local export message")
|
|
|
|
// Verify the output directory exists
|
|
#expect(FileManager.default.fileExists(atPath: existingOutputDir.path), "Expected local output directory to exist")
|
|
|
|
// Verify the existing file is still there (local output should merge/overwrite)
|
|
let contents = try FileManager.default.contentsOfDirectory(atPath: existingOutputDir.path)
|
|
#expect(!contents.isEmpty, "Expected local output directory to contain files")
|
|
|
|
// The behavior may vary - local output might overwrite the directory or merge contents
|
|
// This test verifies that the operation completes successfully with an existing directory
|
|
}
|
|
|
|
@Test func testBuildLocalOutputFailure() throws {
|
|
let tempDir: URL = try createTempDir()
|
|
let dockerfile: String =
|
|
"""
|
|
FROM scratch
|
|
|
|
ADD test.txt /test.txt
|
|
"""
|
|
let context: [FileSystemEntry] = [
|
|
.file("test.txt", content: .data("test\n".data(using: .utf8)!))
|
|
]
|
|
try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context)
|
|
|
|
// Use a path that doesn't exist and can't be created (invalid parent)
|
|
let invalidOutputDir = URL(fileURLWithPath: "/nonexistent/invalid/path")
|
|
let imageName = "local-invalid-test:\(UUID().uuidString)"
|
|
|
|
#expect(throws: CLIError.self) {
|
|
try buildWithLocalOutput(tag: imageName, tempDir: tempDir, outputDir: invalidOutputDir)
|
|
}
|
|
}
|
|
|
|
// Helper function to build with local output
|
|
@discardableResult
|
|
func buildWithLocalOutput(tag: String, tempDir: URL, outputDir: URL, args: [String]? = nil) throws -> String {
|
|
try buildWithPathsAndLocalOutput(
|
|
tag: tag,
|
|
tempContext: tempDir,
|
|
tempDockerfileContext: tempDir,
|
|
outputDir: outputDir,
|
|
args: args
|
|
)
|
|
}
|
|
|
|
// Helper function to build with different paths and local output
|
|
@discardableResult
|
|
func buildWithPathsAndLocalOutput(
|
|
tag: String,
|
|
tempContext: URL,
|
|
tempDockerfileContext: URL,
|
|
outputDir: URL,
|
|
args: [String]? = nil
|
|
) throws -> String {
|
|
let contextDir: URL = tempContext.appendingPathComponent("context")
|
|
let contextDirPath = contextDir.absoluteURL.path
|
|
var buildArgs = [
|
|
"build",
|
|
"-f",
|
|
tempDockerfileContext.appendingPathComponent("Dockerfile").path,
|
|
"-t",
|
|
tag,
|
|
"--output",
|
|
"type=local,dest=\(outputDir.path)",
|
|
]
|
|
if let args = args {
|
|
for arg in args {
|
|
buildArgs.append("--build-arg")
|
|
buildArgs.append(arg)
|
|
}
|
|
}
|
|
buildArgs.append(contextDirPath)
|
|
|
|
let response = try run(arguments: buildArgs)
|
|
if response.status != 0 {
|
|
throw CLIError.executionFailed("build failed: stdout=\(response.output) stderr=\(response.error)")
|
|
}
|
|
|
|
return response.output
|
|
}
|
|
}
|
|
}
|