Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let package = Package(
.library(name: "ContainerNetworkServer", targets: ["ContainerNetworkServer"]),
.library(name: "ContainerNetworkVmnetServer", targets: ["ContainerNetworkVmnetServer"]),
.library(name: "ContainerResource", targets: ["ContainerResource"]),
.library(name: "ContainerTestSupport", targets: ["ContainerTestSupport"]),
.library(name: "ContainerLog", targets: ["ContainerLog"]),
.library(name: "ContainerPersistence", targets: ["ContainerPersistence"]),
.library(name: "ContainerPlugin", targets: ["ContainerPlugin"]),
Expand Down Expand Up @@ -96,6 +97,7 @@ let package = Package(
"ContainerLog",
"ContainerPersistence",
"ContainerResource",
"ContainerTestSupport",
"MachineAPIClient",
"Yams",
],
Expand Down Expand Up @@ -566,7 +568,14 @@ let package = Package(
.target(
name: "ContainerTestSupport",
dependencies: [
.product(name: "SystemPackage", package: "swift-system")
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "TOML", package: "swift-toml"),
"ContainerLog",
"ContainerPersistence",
"ContainerResource",
]
),
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Testing

extension ContainerFixture {
/// A file-system entry to materialize inside a build context directory.
enum FileSystemEntry {
public enum FileSystemEntry {
case file(
_ path: String,
content: FileEntryContent,
Expand All @@ -41,22 +41,23 @@ extension ContainerFixture {
case symbolicLink(_ path: String, target: String, uid: uid_t = 0, gid: gid_t = 0)
}

enum FileEntryContent {
public enum FileEntryContent {
case zeroFilled(size: Int64)
case data(Data)
}

struct FilePermissions: OptionSet {
let rawValue: UInt16
static let r = FilePermissions(rawValue: 0o400)
static let w = FilePermissions(rawValue: 0o200)
static let x = FilePermissions(rawValue: 0o100)
static let gr = FilePermissions(rawValue: 0o040)
static let gw = FilePermissions(rawValue: 0o020)
static let gx = FilePermissions(rawValue: 0o010)
static let or = FilePermissions(rawValue: 0o004)
static let ow = FilePermissions(rawValue: 0o002)
static let ox = FilePermissions(rawValue: 0o001)
public struct FilePermissions: OptionSet, Sendable {
public let rawValue: UInt16
public init(rawValue: UInt16) { self.rawValue = rawValue }
public static let r = FilePermissions(rawValue: 0o400)
public static let w = FilePermissions(rawValue: 0o200)
public static let x = FilePermissions(rawValue: 0o100)
public static let gr = FilePermissions(rawValue: 0o040)
public static let gw = FilePermissions(rawValue: 0o020)
public static let gx = FilePermissions(rawValue: 0o010)
public static let or = FilePermissions(rawValue: 0o004)
public static let ow = FilePermissions(rawValue: 0o002)
public static let ox = FilePermissions(rawValue: 0o001)
}
}

Expand All @@ -65,24 +66,24 @@ extension ContainerFixture {
extension ContainerFixture {

/// Starts the buildkit builder container.
func builderStart(cpus: Int64 = 2, memoryInGBs: Int64 = 2) throws {
public func builderStart(cpus: Int64 = 2, memoryInGBs: Int64 = 2) throws {
try run(["builder", "start", "-c", "\(cpus)", "-m", "\(memoryInGBs)GB"]).check()
}

/// Stops the buildkit builder container.
func builderStop() throws {
public func builderStop() throws {
try run(["builder", "stop"]).check()
}

/// Deletes the buildkit builder container.
func builderDelete(force: Bool = false) throws {
public func builderDelete(force: Bool = false) throws {
var args = ["builder", "delete"]
if force { args.append("--force") }
try run(args).check()
}

/// Polls until the buildkit container is running and the builder shim is ready.
func waitForBuilderRunning() async throws {
public func waitForBuilderRunning() async throws {
try await waitForContainerRunning("buildkit", attempts: 10)
for _ in 0..<3 {
let response = try? doExec("buildkit", cmd: ["pidof", "-s", "container-builder-shim"])
Expand All @@ -99,7 +100,7 @@ extension ContainerFixture {
/// Each build test gets an isolated builder to avoid inter-test contamination.
/// Acquires a process-wide lock so only one test holds the buildkit singleton at a time,
/// regardless of how many suites run concurrently in the global pass.
func withBuilder(
public func withBuilder(
cpus: Int64 = 2,
memoryInGBs: Int64 = 2,
_ body: @Sendable (ContainerFixture) async throws -> Void
Expand All @@ -120,7 +121,7 @@ extension ContainerFixture {
/// Use this in tests that manually manage the builder lifecycle (e.g. lifecycle
/// tests that call ``builderStart()``/``builderStop()`` directly) so they
/// serialise correctly with tests that use ``withBuilder(_:)``.
func withBuilderLock<T: Sendable>(_ body: @Sendable () async throws -> T) async throws -> T {
public func withBuilderLock<T: Sendable>(_ body: @Sendable () async throws -> T) async throws -> T {
try await withoutActuallyEscaping(body) { escapingBody in
try await Self.builderLock.withLock { _ in
try await escapingBody()
Expand All @@ -138,15 +139,15 @@ extension ContainerFixture {
/// Creates a new scratch directory under ``testDir`` and returns its path.
///
/// The directory is removed automatically when the fixture scope exits.
func createTempDir() throws -> FilePath {
public func createTempDir() throws -> FilePath {
let dir = testDir.appending(UUID().uuidString)
try FileManager.default.createDirectory(
atPath: dir.string, withIntermediateDirectories: true, attributes: nil)
return dir
}

/// Writes `contents` to a new file under ``testDir`` with the given suffix.
func createTempFile(suffix: String, contents: Data) throws -> FilePath {
public func createTempFile(suffix: String, contents: Data) throws -> FilePath {
let file = testDir.appending(UUID().uuidString + suffix)
try contents.write(to: URL(filePath: file.string), options: .atomic)
return file
Expand All @@ -156,7 +157,7 @@ extension ContainerFixture {
///
/// Creates `dir/Dockerfile` (if `dockerfile` is non-empty) and
/// `dir/context/` populated with `context` entries.
func createContext(dir: FilePath, dockerfile: String, context: [FileSystemEntry]? = nil) throws {
public func createContext(dir: FilePath, dockerfile: String, context: [FileSystemEntry]? = nil) throws {
if !dockerfile.isEmpty {
try Data(dockerfile.utf8).write(to: URL(filePath: dir.appending("Dockerfile").string), options: .atomic)
}
Expand All @@ -169,7 +170,7 @@ extension ContainerFixture {
}

/// Materializes a ``FileSystemEntry`` inside `contextDir`.
func createEntry(_ entry: FileSystemEntry, contextDir: FilePath) throws {
public func createEntry(_ entry: FileSystemEntry, contextDir: FilePath) throws {
switch entry {
case .file(let path, let content, let permissions, let uid, let gid):
let fullPath = appendingRelative(contextDir, path)
Expand Down Expand Up @@ -242,7 +243,7 @@ extension ContainerFixture {

/// Builds an image from `contextDir/Dockerfile` with context `contextDir/context/`.
@discardableResult
func build(
public func build(
tag: String,
contextDir: FilePath = FilePath("."),
buildArgs: [String] = [],
Expand All @@ -260,7 +261,7 @@ extension ContainerFixture {
/// - `contextDir` defaults to the current directory (`.`)
/// - `dockerfilePath` defaults to `nil`, resolved to `contextDir/Dockerfile` at call time
@discardableResult
func buildWithPaths(
public func buildWithPaths(
tags: [String] = [],
contextDir: FilePath = FilePath("."),
dockerfilePath: FilePath? = nil,
Expand All @@ -284,7 +285,7 @@ extension ContainerFixture {

/// Builds with a Dockerfile read from stdin.
@discardableResult
func buildWithStdin(
public func buildWithStdin(
tags: [String],
contextDir: FilePath,
dockerfileContents: String,
Expand All @@ -307,7 +308,7 @@ extension ContainerFixture {

/// Builds with `--output type=local,dest=<outputDir>`.
@discardableResult
func buildWithPathsAndLocalOutput(
public func buildWithPathsAndLocalOutput(
tag: String,
contextDir: FilePath = FilePath("."),
dockerfilePath: FilePath? = nil,
Expand Down Expand Up @@ -337,18 +338,18 @@ extension ContainerFixture {

extension ContainerFixture {
/// Returns true if `path` exists as a regular file inside `container`.
func containerHasFile(_ container: String, at path: String) throws -> Bool {
public func containerHasFile(_ container: String, at path: String) throws -> Bool {
try run(["exec", container, "test", "-f", path]).status == 0
}

/// Asserts that `path` exists as a regular file inside `container`.
func assertContainerHasFile(_ container: String, at path: String, _ comment: String? = nil) throws {
public func assertContainerHasFile(_ container: String, at path: String, _ comment: String? = nil) throws {
let exists = try containerHasFile(container, at: path)
#expect(exists, "\(comment ?? path) should exist in container")
}

/// Asserts that `path` does NOT exist inside `container`.
func assertContainerMissingFile(_ container: String, at path: String, _ comment: String? = nil) throws {
public func assertContainerMissingFile(_ container: String, at path: String, _ comment: String? = nil) throws {
let exists = try containerHasFile(container, at: path)
#expect(!exists, "\(comment ?? path) should NOT exist in container")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@

import Foundation

struct CommandResult: Sendable {
let outputData: Data
let errorData: Data
let status: Int32
public struct CommandResult: Sendable {
public let outputData: Data
public let errorData: Data
public let status: Int32

var output: String {
public var output: String {
String(data: outputData, encoding: .utf8) ?? ""
}

var error: String {
public var error: String {
String(data: errorData, encoding: .utf8) ?? ""
}

@discardableResult
func check(_ message: String? = nil) throws -> CommandResult {
public func check(_ message: String? = nil) throws -> CommandResult {
guard status == 0 else {
let detail = message ?? error.trimmingCharacters(in: .whitespacesAndNewlines)
throw CommandError.nonZeroExit(status, detail)
Expand All @@ -39,7 +39,7 @@ struct CommandResult: Sendable {
}
}

enum CommandError: Error {
public enum CommandError: Error {
case binaryNotFound
case executionFailed(String)
case nonZeroExit(Int32, String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import SystemPackage

extension ContainerFixture {
/// Decoded output of `container inspect <name>`.
struct InspectOutput: Codable {
struct Status: Codable {
let state: String
let networks: [ContainerResource.Attachment]
public struct InspectOutput: Codable {
public struct Status: Codable {
public let state: String
public let networks: [ContainerResource.Attachment]
}
let configuration: ContainerConfiguration
let status: Status
var networks: [ContainerResource.Attachment] { status.networks }
public let configuration: ContainerConfiguration
public let status: Status
public var networks: [ContainerResource.Attachment] { status.networks }
}
}

Expand All @@ -38,7 +38,7 @@ extension ContainerFixture {
extension ContainerFixture {

/// `-e` flags forwarding proxy env vars into container commands.
var proxyEnvironmentArgs: [String] {
public var proxyEnvironmentArgs: [String] {
let vars = Set(["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"])
return ProcessInfo.processInfo.environment
.filter { vars.contains($0.key) }
Expand All @@ -49,7 +49,7 @@ extension ContainerFixture {
///
/// `containerEnv` injects environment variables into the container via `-e` flags.
/// To set the CLI subprocess environment (e.g. for `--ssh`), use ``run(_:env:)`` directly.
func doLongRun(
public func doLongRun(
name: String,
image: String? = nil,
args: [String] = [],
Expand All @@ -70,7 +70,7 @@ extension ContainerFixture {
}

/// Creates a stopped container (`container create`).
func doCreate(
public func doCreate(
name: String,
image: String? = nil,
args: [String] = ["sleep", "infinity"],
Expand All @@ -90,20 +90,20 @@ extension ContainerFixture {
}

/// Starts a stopped container.
func doStart(_ name: String) throws {
public func doStart(_ name: String) throws {
try run(["start", name]).check()
}

/// Stops a container. Pass `signal: nil` to use the server's default.
func doStop(_ name: String, signal: String? = "SIGKILL") throws {
public func doStop(_ name: String, signal: String? = "SIGKILL") throws {
var args = ["stop"]
if let signal { args += ["-s", signal] }
args.append(name)
try run(args).check()
}

/// Deletes a container.
func doRemove(_ name: String, force: Bool = false) throws {
public func doRemove(_ name: String, force: Bool = false) throws {
var args = ["delete"]
if force { args.append("--force") }
args.append(name)
Expand All @@ -116,7 +116,7 @@ extension ContainerFixture {
/// this when the container is expected to exist and removal must succeed.
/// Set `ignoreFailure: true` in cleanup contexts where best-effort removal
/// is acceptable (e.g. the container may have already been removed).
func doRemoveIfExists(_ name: String, force: Bool = false, ignoreFailure: Bool = false) throws {
public func doRemoveIfExists(_ name: String, force: Bool = false, ignoreFailure: Bool = false) throws {
do {
try doRemove(name, force: force)
} catch {
Expand All @@ -126,7 +126,7 @@ extension ContainerFixture {

/// Runs a command inside a container, returns stdout. Throws on non-zero exit.
@discardableResult
func doExec(
public func doExec(
_ name: String,
cmd: [String],
detach: Bool = false,
Expand All @@ -142,7 +142,7 @@ extension ContainerFixture {
}

/// Exports a container filesystem to a tar archive at `path`.
func doExport(_ name: String, to path: FilePath) throws {
public func doExport(_ name: String, to path: FilePath) throws {
try run(["export", name, "-o", path.string]).check()
}
}
Expand All @@ -152,7 +152,7 @@ extension ContainerFixture {
extension ContainerFixture {

/// Returns the parsed inspect output for a container.
func inspectContainer(_ name: String) throws -> InspectOutput {
public func inspectContainer(_ name: String) throws -> InspectOutput {
let result = try run(["inspect", name]).check()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
Expand All @@ -164,12 +164,12 @@ extension ContainerFixture {
}

/// Returns the `status.state` string for a container (e.g. `"running"`, `"stopped"`).
func getContainerStatus(_ name: String) throws -> String {
public func getContainerStatus(_ name: String) throws -> String {
try inspectContainer(name).status.state
}

/// Returns the `configuration.id` for a container.
func getContainerId(_ name: String) throws -> String {
public func getContainerId(_ name: String) throws -> String {
try inspectContainer(name).configuration.id
}
}
Loading
Loading