Files
apple--containerization/Sources/Integration/PodTests.swift
T
wehub-resource-sync 680845cb1c
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

2225 lines
86 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ArgumentParser
import Containerization
import ContainerizationEXT4
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Logging
import SystemPackage
extension IntegrationSuite {
/// Clone a rootfs mount to a new location for use by a container in a pod
private func cloneRootfs(_ rootfs: Containerization.Mount, testID: String, containerID: String) throws -> Containerization.Mount {
let clonePath = Self.testDir.appending(component: "\(testID)-\(containerID).ext4").absolutePath()
try? FileManager.default.removeItem(atPath: clonePath)
return try rootfs.clone(to: clonePath)
}
func testPodSingleContainer() async throws {
let id = "test-pod-single-container"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/true"]
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
}
func testPodMultipleContainers() async throws {
let id = "test-pod-multiple-containers"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/true"]
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/echo", "hello"]
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 status \(status1) != 0")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 status \(status2) != 0")
}
}
func testPodContainerOutput() async throws {
let id = "test-pod-container-output"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/echo", "hello from pod"]
config.process.stdout = buffer
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "hello from pod\n" else {
throw IntegrationError.assert(
msg: "process should have returned on stdout 'hello from pod' != '\(String(data: buffer.data, encoding: .utf8)!)'")
}
}
func testPodConcurrentContainers() async throws {
let id = "test-pod-concurrent-containers"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
// Add 5 containers
for i in 0..<5 {
try await pod.addContainer("container\(i)", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container\(i)")) { config in
config.process.arguments = ["/bin/sleep", "1"]
}
}
try await pod.create()
// Start all containers concurrently
try await withThrowingTaskGroup(of: Void.self) { group in
for i in 0..<5 {
group.addTask {
try await pod.startContainer("container\(i)")
}
}
try await group.waitForAll()
}
// Wait for all containers concurrently
try await withThrowingTaskGroup(of: Void.self) { group in
for i in 0..<5 {
group.addTask {
let status = try await pod.waitContainer("container\(i)")
if status.exitCode != 0 {
throw IntegrationError.assert(msg: "container\(i) status \(status) != 0")
}
}
}
try await group.waitForAll()
}
try await pod.stop()
}
func testPodExecInContainer() async throws {
let id = "test-pod-exec-in-container"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "100"]
}
try await pod.create()
try await pod.startContainer("container1")
let buffer = BufferWriter()
let exec = try await pod.execInContainer("container1", processID: "exec1") { config in
config.arguments = ["/bin/echo", "exec test"]
config.stdout = buffer
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "exec status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "exec test\n" else {
throw IntegrationError.assert(
msg: "exec should have returned 'exec test' != '\(String(data: buffer.data, encoding: .utf8)!)'")
}
}
func testPodExecInContainerEnv() async throws {
let id = "test-pod-exec-in-container-env"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "100"]
config.process.environmentVariables.append("MY_VAR=hello_from_container")
}
try await pod.create()
try await pod.startContainer("container1")
let buffer = BufferWriter()
let exec = try await pod.execInContainer("container1", processID: "exec1") { config in
config.arguments = ["/bin/sh", "-c", "printenv MY_VAR"]
config.stdout = buffer
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "exec env status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "hello_from_container\n" else {
throw IntegrationError.assert(
msg: "exec should have inherited container env MY_VAR=hello_from_container, got '\(String(data: buffer.data, encoding: .utf8) ?? "nil")'")
}
}
func testPodContainerHostname() async throws {
let id = "test-pod-container-hostname"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/hostname"]
config.hostname = "my-pod-container"
config.process.stdout = buffer
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "my-pod-container\n" else {
throw IntegrationError.assert(
msg: "hostname should be 'my-pod-container' != '\(String(data: buffer.data, encoding: .utf8)!)'")
}
}
func testPodContainerHostnameDefaultsToContainerID() async throws {
let id = "test-pod-container-hostname-default"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/hostname"]
config.process.stdout = buffer
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "container1\n" else {
throw IntegrationError.assert(
msg: "hostname should default to container id 'container1', got '\(String(data: buffer.data, encoding: .utf8)!)'")
}
}
func testPodStopContainerIdempotency() async throws {
let id = "test-pod-stop-container-idempotency"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/true"]
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
// Stop container twice - should not fail
try await pod.stopContainer("container1")
try await pod.stopContainer("container1")
try await pod.stop()
}
func testPodListContainers() async throws {
let id = "test-pod-list-containers"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let containerIDs = ["container1", "container2", "container3"]
for containerID in containerIDs {
try await pod.addContainer(containerID, rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: containerID)) { config in
config.process.arguments = ["/bin/true"]
}
}
let listedContainers = await pod.listContainers()
guard Set(listedContainers) == Set(containerIDs) else {
throw IntegrationError.assert(
msg: "listed containers \(listedContainers) != expected \(containerIDs)")
}
try await pod.create()
try await pod.stop()
}
func testPodContainerStatistics() async throws {
let id = "test-pod-container-statistics"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await pod.startContainer("container2")
let stats = try await pod.statistics()
guard stats.count == 2 else {
throw IntegrationError.assert(msg: "expected 2 container stats, got \(stats.count)")
}
let containerIDs = Set(stats.map { $0.id })
guard containerIDs == Set(["container1", "container2"]) else {
throw IntegrationError.assert(msg: "unexpected container IDs in stats: \(containerIDs)")
}
for stat in stats {
guard let process = stat.process, process.current > 0 else {
throw IntegrationError.assert(msg: "container \(stat.id) process count should be > 0")
}
guard let memory = stat.memory, memory.usageBytes > 0 else {
throw IntegrationError.assert(msg: "container \(stat.id) memory usage should be > 0")
}
print("Container \(stat.id) statistics:")
print(" Processes: \(process.current)")
print(" Memory: \(memory.usageBytes) bytes")
print(" CPU: \(stat.cpu?.usageUsec ?? 0) usec")
}
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodMemoryEventsOOMKill() async throws {
let id = "test-pod-memory-events-oom-kill"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
do {
try await pod.create()
try await pod.startContainer("container1")
let exec = try await pod.execInContainer("container1", processID: "oom-trigger") { config in
config.arguments = [
"sh",
"-c",
"echo 2097152 > /sys/fs/cgroup/memory.max && dd if=/dev/zero of=/dev/null bs=100M",
]
}
try await exec.start()
let status = try await exec.wait()
if status.exitCode == 0 {
throw IntegrationError.assert(msg: "expected exit code > 0")
}
try await exec.delete()
let stats = try await pod.statistics(containerIDs: ["container1"], categories: .memoryEvents)
guard let containerStats = stats.first, let events = containerStats.memoryEvents else {
throw IntegrationError.assert(msg: "expected memoryEvents to be present")
}
print("Memory events for pod container container1:")
print(" low: \(events.low)")
print(" high: \(events.high)")
print(" max: \(events.max)")
print(" oom: \(events.oom)")
print(" oomKill: \(events.oomKill)")
guard events.oomKill > 0 else {
throw IntegrationError.assert(msg: "expected oomKill > 0, got \(events.oomKill)")
}
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodContainerResourceLimits() async throws {
let id = "test-pod-container-resource-limits"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
config.cpus = 2
config.memoryInBytes = 256.mib()
}
do {
try await pod.create()
try await pod.startContainer("container1")
// Verify memory limit
let memoryBuffer = BufferWriter()
let memoryExec = try await pod.execInContainer("container1", processID: "check-memory") { config in
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
config.stdout = memoryBuffer
}
try await memoryExec.start()
var status = try await memoryExec.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-memory status \(status) != 0")
}
try await memoryExec.delete()
guard let memoryLimit = String(data: memoryBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse memory.max")
}
let expectedMemory = "\(256.mib())"
guard memoryLimit == expectedMemory else {
throw IntegrationError.assert(msg: "memory.max \(memoryLimit) != expected \(expectedMemory)")
}
// Verify CPU limit
let cpuBuffer = BufferWriter()
let cpuExec = try await pod.execInContainer("container1", processID: "check-cpu") { config in
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
config.stdout = cpuBuffer
}
try await cpuExec.start()
status = try await cpuExec.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-cpu status \(status) != 0")
}
try await cpuExec.delete()
guard let cpuLimit = String(data: cpuBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse cpu.max")
}
let expectedCpu = "200000 100000" // 2 CPUs: quota=200000, period=100000
guard cpuLimit == expectedCpu else {
throw IntegrationError.assert(msg: "cpu.max '\(cpuLimit)' != expected '\(expectedCpu)'")
}
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodContainerFilesystemIsolation() async throws {
let id = "test-pod-container-filesystem-isolation"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await pod.startContainer("container2")
// Write a file in container1
let writeExec = try await pod.execInContainer("container1", processID: "write-file") { config in
config.arguments = ["sh", "-c", "echo 'secret data' > /tmp/container1-secret.txt"]
}
try await writeExec.start()
var status = try await writeExec.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "write-file status \(status) != 0")
}
try await writeExec.delete()
// Verify the file exists in container1
let readBuffer1 = BufferWriter()
let readExec1 = try await pod.execInContainer("container1", processID: "read-file-1") { config in
config.arguments = ["cat", "/tmp/container1-secret.txt"]
config.stdout = readBuffer1
}
try await readExec1.start()
status = try await readExec1.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "read-file-1 status \(status) != 0")
}
try await readExec1.delete()
guard String(data: readBuffer1.data, encoding: .utf8) == "secret data\n" else {
throw IntegrationError.assert(msg: "file content in container1 should be 'secret data'")
}
// Try to read the file from container2 - should fail
let readExec2 = try await pod.execInContainer("container2", processID: "read-file-2") { config in
config.arguments = ["cat", "/tmp/container1-secret.txt"]
}
try await readExec2.start()
status = try await readExec2.wait()
try await readExec2.delete()
// File should NOT exist in container2, so cat should fail
guard status.exitCode != 0 else {
throw IntegrationError.assert(msg: "file should NOT be accessible from container2")
}
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodContainerPIDNamespaceIsolation() async throws {
let id = "test-pod-container-pid-isolation"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await pod.startContainer("container2")
// Start a unique process in container1
let sleepExec1 = try await pod.execInContainer("container1", processID: "unique-sleep-1") { config in
config.arguments = ["/bin/sleep", "9999"]
}
try await sleepExec1.start()
// List processes in container1 - should see sleep 9999
let ps1Buffer = BufferWriter()
let psExec1 = try await pod.execInContainer("container1", processID: "ps-1") { config in
config.arguments = ["ps", "aux"]
config.stdout = ps1Buffer
}
try await psExec1.start()
var status = try await psExec1.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "ps-1 status \(status) != 0")
}
try await psExec1.delete()
guard let ps1Output = String(data: ps1Buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to parse ps output from container1")
}
// Verify sleep 9999 is visible in container1
guard ps1Output.contains("sleep 9999") else {
throw IntegrationError.assert(msg: "sleep 9999 should be visible in container1")
}
// List processes in container2 - should NOT see sleep 9999
let ps2Buffer = BufferWriter()
let psExec2 = try await pod.execInContainer("container2", processID: "ps-2") { config in
config.arguments = ["ps", "aux"]
config.stdout = ps2Buffer
}
try await psExec2.start()
status = try await psExec2.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "ps-2 status \(status) != 0")
}
try await psExec2.delete()
guard let ps2Output = String(data: ps2Buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to parse ps output from container2")
}
// Verify sleep 9999 is NOT visible in container2
guard !ps2Output.contains("sleep 9999") else {
throw IntegrationError.assert(msg: "sleep 9999 should NOT be visible in container2 (PID namespace isolation failed)")
}
try await sleepExec1.delete()
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodContainerIndependentResourceLimits() async throws {
let id = "test-pod-container-independent-limits"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
// Container1 with 1 CPU and 128 MiB memory
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
config.cpus = 1
config.memoryInBytes = 128.mib()
}
// Container2 with 2 CPUs and 256 MiB memory
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/sleep", "infinity"]
config.cpus = 2
config.memoryInBytes = 256.mib()
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await pod.startContainer("container2")
// Verify container1 memory limit
let mem1Buffer = BufferWriter()
let memExec1 = try await pod.execInContainer("container1", processID: "check-mem-1") { config in
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
config.stdout = mem1Buffer
}
try await memExec1.start()
var status = try await memExec1.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-mem-1 status \(status) != 0")
}
try await memExec1.delete()
guard let mem1Limit = String(data: mem1Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse memory.max from container1")
}
let expectedMem1 = "\(128.mib())"
guard mem1Limit == expectedMem1 else {
throw IntegrationError.assert(msg: "container1 memory.max \(mem1Limit) != expected \(expectedMem1)")
}
// Verify container1 CPU limit
let cpu1Buffer = BufferWriter()
let cpuExec1 = try await pod.execInContainer("container1", processID: "check-cpu-1") { config in
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
config.stdout = cpu1Buffer
}
try await cpuExec1.start()
status = try await cpuExec1.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-cpu-1 status \(status) != 0")
}
try await cpuExec1.delete()
guard let cpu1Limit = String(data: cpu1Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse cpu.max from container1")
}
let expectedCpu1 = "100000 100000" // 1 CPU
guard cpu1Limit == expectedCpu1 else {
throw IntegrationError.assert(msg: "container1 cpu.max '\(cpu1Limit)' != expected '\(expectedCpu1)'")
}
// Verify container2 memory limit
let mem2Buffer = BufferWriter()
let memExec2 = try await pod.execInContainer("container2", processID: "check-mem-2") { config in
config.arguments = ["cat", "/sys/fs/cgroup/memory.max"]
config.stdout = mem2Buffer
}
try await memExec2.start()
status = try await memExec2.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-mem-2 status \(status) != 0")
}
try await memExec2.delete()
guard let mem2Limit = String(data: mem2Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse memory.max from container2")
}
let expectedMem2 = "\(256.mib())"
guard mem2Limit == expectedMem2 else {
throw IntegrationError.assert(msg: "container2 memory.max \(mem2Limit) != expected \(expectedMem2)")
}
// Verify container2 CPU limit
let cpu2Buffer = BufferWriter()
let cpuExec2 = try await pod.execInContainer("container2", processID: "check-cpu-2") { config in
config.arguments = ["cat", "/sys/fs/cgroup/cpu.max"]
config.stdout = cpu2Buffer
}
try await cpuExec2.start()
status = try await cpuExec2.wait()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "check-cpu-2 status \(status) != 0")
}
try await cpuExec2.delete()
guard let cpu2Limit = String(data: cpu2Buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to parse cpu.max from container2")
}
let expectedCpu2 = "200000 100000" // 2 CPUs
guard cpu2Limit == expectedCpu2 else {
throw IntegrationError.assert(msg: "container2 cpu.max '\(cpu2Limit)' != expected '\(expectedCpu2)'")
}
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodSharedPIDNamespace() async throws {
let id = "test-pod-shared-pid-namespace"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
config.shareProcessNamespace = true
}
// First container runs a long-running process
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/sleep", "300"]
}
// Second container checks if it can see container1's sleep process
let psBuffer = BufferWriter()
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/sh", "-c", "ps aux | grep 'sleep 300' | grep -v grep"]
config.process.stdout = psBuffer
}
try await pod.create()
try await pod.startContainer("container1")
try await Task.sleep(for: .milliseconds(100))
try await pod.startContainer("container2")
let status = try await pod.waitContainer("container2")
try await pod.killContainer("container1", signal: .kill)
_ = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 should have found the sleep process (status: \(status))")
}
let output = String(data: psBuffer.data, encoding: .utf8) ?? ""
guard output.contains("sleep 300") else {
throw IntegrationError.assert(msg: "ps output should contain 'sleep 300', got: '\(output)'")
}
}
func testPodReadOnlyRootfs() async throws {
let id = "test-pod-readonly-rootfs"
let bs = try await bootstrap(id)
var rootfs = bs.rootfs
rootfs.options.append("ro")
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: rootfs) { config in
config.process.arguments = ["touch", "/testfile"]
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
// touch should fail on a read-only rootfs
guard status.exitCode != 0 else {
throw IntegrationError.assert(msg: "touch should have failed on read-only rootfs")
}
}
func testPodReadOnlyRootfsDNSConfigured() async throws {
let id = "test-pod-readonly-rootfs-dns"
let bs = try await bootstrap(id)
var rootfs = bs.rootfs
rootfs.options.append("ro")
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: rootfs) { config in
// Verify /etc/resolv.conf was written before rootfs was remounted read-only
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer
config.dns = DNS(nameservers: ["8.8.8.8", "8.8.4.4"])
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "cat /etc/resolv.conf failed with status \(status)")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard output.contains("8.8.8.8") && output.contains("8.8.4.4") else {
throw IntegrationError.assert(msg: "expected /etc/resolv.conf to contain DNS servers, got: \(output)")
}
}
func testPodSingleFileMount() async throws {
let id = "test-pod-single-file-mount"
let bs = try await bootstrap(id)
// Create a temp file with known content
let testContent = "Hello from pod single file mount!"
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("pod-config.txt")
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["cat", "/etc/myconfig.txt"]
// Mount a single file using virtiofs share
config.mounts.append(.share(source: hostFile.path, destination: "/etc/myconfig.txt"))
config.process.stdout = buffer
}
do {
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
}
guard output == testContent else {
throw IntegrationError.assert(
msg: "expected '\(testContent)', got '\(output)'")
}
} catch {
try? await pod.stop()
throw error
}
}
func testPodContainerHostsConfig() async throws {
let id = "test-pod-container-hosts"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry.localHostIPV6(),
Hosts.Entry(ipAddress: "10.0.0.50", hostnames: ["myservice.local", "myservice"]),
])
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "cat /etc/hosts failed with status \(status)")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard output.contains("10.0.0.50") && output.contains("myservice.local") else {
throw IntegrationError.assert(msg: "expected /etc/hosts to contain custom entry, got: \(output)")
}
}
func testPodMultipleContainersDifferentDNS() async throws {
let id = "test-pod-multi-dns"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer1
config.dns = DNS(nameservers: ["1.1.1.1"])
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer2
config.dns = DNS(nameservers: ["8.8.8.8"])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
guard output1.contains("1.1.1.1") && !output1.contains("8.8.8.8") else {
throw IntegrationError.assert(msg: "container1 should have 1.1.1.1 DNS, got: \(output1)")
}
guard output2.contains("8.8.8.8") && !output2.contains("1.1.1.1") else {
throw IntegrationError.assert(msg: "container2 should have 8.8.8.8 DNS, got: \(output2)")
}
}
func testPodMultipleContainersDifferentHosts() async throws {
let id = "test-pod-multi-hosts"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer1
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.1", hostnames: ["service-a.local", "service-a"]),
])
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer2
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.2", hostnames: ["service-b.local", "service-b"]),
])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
guard output1.contains("10.0.0.1") && output1.contains("service-a.local") else {
throw IntegrationError.assert(msg: "container1 should have service-a entry, got: \(output1)")
}
guard !output1.contains("10.0.0.2") && !output1.contains("service-b") else {
throw IntegrationError.assert(msg: "container1 should NOT have service-b entry, got: \(output1)")
}
guard output2.contains("10.0.0.2") && output2.contains("service-b.local") else {
throw IntegrationError.assert(msg: "container2 should have service-b entry, got: \(output2)")
}
guard !output2.contains("10.0.0.1") && !output2.contains("service-a") else {
throw IntegrationError.assert(msg: "container2 should NOT have service-a entry, got: \(output2)")
}
}
func testPodLevelDNS() async throws {
let id = "test-pod-level-dns"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set DNS at the pod level
config.dns = DNS(nameservers: ["9.9.9.9", "149.112.112.112"])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Neither container specifies DNS. We should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer1
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer2
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Both containers should have the pod-level DNS
guard output1.contains("9.9.9.9") && output1.contains("149.112.112.112") else {
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
}
guard output2.contains("9.9.9.9") && output2.contains("149.112.112.112") else {
throw IntegrationError.assert(msg: "container2 should have pod-level DNS (9.9.9.9), got: \(output2)")
}
}
func testPodLevelDNSWithContainerOverride() async throws {
let id = "test-pod-level-dns-override"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set DNS at the pod level
config.dns = DNS(nameservers: ["9.9.9.9"])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 does NOT specify DNS. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer1
}
// Container2 specifies its own DNS. It should override pod-level
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer2
config.dns = DNS(nameservers: ["8.8.8.8"])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Container1 should have pod-level DNS
guard output1.contains("9.9.9.9") && !output1.contains("8.8.8.8") else {
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
}
// Container2 should have its own DNS, not pod-level
guard output2.contains("8.8.8.8") && !output2.contains("9.9.9.9") else {
throw IntegrationError.assert(msg: "container2 should have container-level DNS (8.8.8.8), got: \(output2)")
}
}
func testPodLevelHosts() async throws {
let id = "test-pod-level-hosts"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hosts at the pod level
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Neither container specifies hosts. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer1
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer2
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Both containers should have the pod-level hosts entry
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
}
guard output2.contains("10.0.0.100") && output2.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container2 should have pod-level hosts entry, got: \(output2)")
}
}
func testPodLevelHostsWithContainerOverride() async throws {
let id = "test-pod-level-hosts-override"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hosts at the pod level
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 does NOT specify hosts. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer1
}
// Container2 specifies its own hosts. It should override pod-level
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer2
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.200", hostnames: ["container-specific.local"]),
])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Container1 should have pod-level hosts entry
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
}
guard !output1.contains("10.0.0.200") && !output1.contains("container-specific.local") else {
throw IntegrationError.assert(msg: "container1 should NOT have container2's hosts entry, got: \(output1)")
}
// Container2 should have its own hosts entry, not pod-level
guard output2.contains("10.0.0.200") && output2.contains("container-specific.local") else {
throw IntegrationError.assert(msg: "container2 should have container-level hosts entry, got: \(output2)")
}
guard !output2.contains("10.0.0.100") && !output2.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container2 should NOT have pod-level hosts entry, got: \(output2)")
}
}
func testPodLevelHostname() async throws {
let id = "test-pod-level-hostname"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hostname at the pod level
config.hostname = "pod-host"
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Neither container specifies a hostname. Both should inherit from pod.
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/hostname"]
config.process.stdout = buffer1
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/hostname"]
config.process.stdout = buffer2
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 hostname failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 hostname failed with status \(status2)")
}
guard String(data: buffer1.data, encoding: .utf8) == "pod-host\n" else {
throw IntegrationError.assert(msg: "container1 should have pod-level hostname 'pod-host', got: '\(String(data: buffer1.data, encoding: .utf8) ?? "nil")'")
}
guard String(data: buffer2.data, encoding: .utf8) == "pod-host\n" else {
throw IntegrationError.assert(msg: "container2 should have pod-level hostname 'pod-host', got: '\(String(data: buffer2.data, encoding: .utf8) ?? "nil")'")
}
}
func testPodLevelHostnameWithContainerOverride() async throws {
let id = "test-pod-level-hostname-override"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hostname at the pod level
config.hostname = "pod-host"
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 does NOT specify a hostname. It should inherit from pod.
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/hostname"]
config.process.stdout = buffer1
}
// Container2 specifies its own hostname. It should override the pod-level value.
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/hostname"]
config.process.stdout = buffer2
config.hostname = "container-host"
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 hostname failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 hostname failed with status \(status2)")
}
// Container1 should have the pod-level hostname
guard String(data: buffer1.data, encoding: .utf8) == "pod-host\n" else {
throw IntegrationError.assert(msg: "container1 should have pod-level hostname 'pod-host', got: '\(String(data: buffer1.data, encoding: .utf8) ?? "nil")'")
}
// Container2 should have its own hostname, not the pod-level one
guard String(data: buffer2.data, encoding: .utf8) == "container-host\n" else {
throw IntegrationError.assert(msg: "container2 should have container-level hostname 'container-host', got: '\(String(data: buffer2.data, encoding: .utf8) ?? "nil")'")
}
}
func testPodRLimitOpenFiles() async throws {
let id = "test-pod-rlimit-open-files"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["sh", "-c", "ulimit -n"]
config.process.rlimits = [
LinuxRLimit(kind: .openFiles, hard: 2048, soft: 1024)
]
config.process.stdout = buffer
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
// ulimit -n returns the soft limit
guard output == "1024" else {
throw IntegrationError.assert(msg: "expected soft limit '1024', got '\(output)'")
}
}
func testPodRLimitExec() async throws {
let id = "test-pod-rlimit-exec"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["sleep", "100"]
}
do {
try await pod.create()
try await pod.startContainer("container1")
// Exec a process with rlimits set
let buffer = BufferWriter()
let exec = try await pod.execInContainer("container1", processID: "rlimit-exec") { config in
config.arguments = ["sh", "-c", "ulimit -n"]
config.rlimits = [
LinuxRLimit(kind: .openFiles, hard: 512, soft: 256)
]
config.stdout = buffer
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "exec status \(status) != 0")
}
guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard output == "256" else {
throw IntegrationError.assert(msg: "expected soft limit '256', got '\(output)'")
}
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodUseInitBasic() async throws {
let id = "test-pod-use-init-basic"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/echo", "hello from pod init"]
config.process.stdout = buffer
config.useInit = true
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard String(data: buffer.data, encoding: .utf8) == "hello from pod init\n" else {
throw IntegrationError.assert(
msg: "expected 'hello from pod init', got '\(String(data: buffer.data, encoding: .utf8) ?? "nil")'")
}
}
func testPodUseInitExitCodePropagation() async throws {
let id = "test-pod-use-init-exit-code"
let bs = try await bootstrap(id)
// Test exit code 0
var pod = try LinuxPod("\(id)-success", vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "success")) { config in
config.process.arguments = ["/bin/true"]
config.useInit = true
}
try await pod.create()
try await pod.startContainer("container1")
var status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "expected exit code 0, got \(status.exitCode)")
}
// Test non-zero exit code
pod = try LinuxPod("\(id)-failure", vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "failure")) { config in
config.process.arguments = ["/bin/false"]
config.useInit = true
}
try await pod.create()
try await pod.startContainer("container1")
status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 1 else {
throw IntegrationError.assert(msg: "expected exit code 1, got \(status.exitCode)")
}
// Test custom exit code
pod = try LinuxPod("\(id)-custom", vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "custom")) { config in
config.process.arguments = ["sh", "-c", "exit 42"]
config.useInit = true
}
try await pod.create()
try await pod.startContainer("container1")
status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 42 else {
throw IntegrationError.assert(msg: "expected exit code 42, got \(status.exitCode)")
}
}
func testPodUseInitSignalForwarding() async throws {
let id = "test-pod-use-init-signal"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["sleep", "300"]
config.useInit = true
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await Task.sleep(for: .milliseconds(100))
// Send SIGTERM, should be forwarded to the child and cause exit
try await pod.killContainer("container1", signal: .term)
let status = try await pod.waitContainer("container1", timeoutInSeconds: 5)
try await pod.stop()
// SIGTERM should result in exit code 128 + 15 = 143
guard status.exitCode == 143 else {
throw IntegrationError.assert(msg: "expected exit code 143 (SIGTERM), got \(status.exitCode)")
}
} catch {
try? await pod.stop()
throw error
}
}
func testPodUseInitMultipleContainers() async throws {
let id = "test-pod-use-init-multiple"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 with useInit
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["/bin/echo", "container1 with init"]
config.process.stdout = buffer1
config.useInit = true
}
// Container2 without useInit
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["/bin/echo", "container2 without init"]
config.process.stdout = buffer2
config.useInit = false
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 exit code \(status1.exitCode) != 0")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 exit code \(status2.exitCode) != 0")
}
guard String(data: buffer1.data, encoding: .utf8) == "container1 with init\n" else {
throw IntegrationError.assert(
msg: "container1 output mismatch: '\(String(data: buffer1.data, encoding: .utf8) ?? "nil")'")
}
guard String(data: buffer2.data, encoding: .utf8) == "container2 without init\n" else {
throw IntegrationError.assert(
msg: "container2 output mismatch: '\(String(data: buffer2.data, encoding: .utf8) ?? "nil")'")
}
}
func testPodUseInitWithSharedPIDNamespace() async throws {
let id = "test-pod-use-init-shared-pid"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
config.shareProcessNamespace = true
}
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["sleep", "300"]
config.useInit = true
}
let psBuffer = BufferWriter()
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
// Check if we can see container1's sleep process through the shared PID namespace
config.process.arguments = ["sh", "-c", "ps aux | grep 'sleep 300' | grep -v grep"]
config.process.stdout = psBuffer
}
try await pod.create()
try await pod.startContainer("container1")
try await Task.sleep(for: .milliseconds(100))
try await pod.startContainer("container2")
let status = try await pod.waitContainer("container2")
try await pod.killContainer("container1", signal: .kill)
_ = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 should have found the sleep process (status: \(status))")
}
let output = String(data: psBuffer.data, encoding: .utf8) ?? ""
guard output.contains("sleep 300") else {
throw IntegrationError.assert(msg: "ps output should contain 'sleep 300', got: '\(output)'")
}
}
func testPodUnixSocketIntoGuestSymlink() async throws {
let id = "test-pod-unixsocket-into-guest-symlink"
let bs = try await bootstrap(id)
let hostSocketPath = try createPodHostUnixSocket()
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
// Use /var/run/test.sock. Alpine has /var/run -> /run symlink
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["sleep", "100"]
config.sockets = [
UnixSocketConfiguration(
source: URL(filePath: hostSocketPath),
destination: URL(filePath: "/var/run/test.sock"),
direction: .into
)
]
}
do {
try await pod.create()
try await pod.startContainer("container1")
let buffer = BufferWriter()
let lsExec = try await pod.execInContainer("container1", processID: "ls-socket") { config in
config.arguments = ["ls", "-l", "/var/run/test.sock"]
config.stdout = buffer
}
try await lsExec.start()
let status2 = try await lsExec.wait()
try await lsExec.delete()
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "ls command failed with status \(status2)")
}
guard let lsOutput = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert ls output to UTF8")
}
guard lsOutput.hasPrefix("s") else {
throw IntegrationError.assert(
msg: "expected socket file (starting with 's'), got: \(lsOutput)")
}
try await pod.killContainer("container1", signal: .kill)
_ = try await pod.waitContainer("container1")
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
private func createPodHostUnixSocket() throws -> String {
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
let socketPath = dir.appendingPathComponent("test.sock").path
let socket = try Socket(type: UnixType(path: socketPath))
try socket.listen()
return socketPath
}
func testPodSysctl() async throws {
let id = "test-pod-sysctl"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.sysctl = [
"net.core.somaxconn": "4096"
]
config.process.arguments = ["cat", "/proc/sys/net/core/somaxconn"]
config.process.stdout = buffer
}
do {
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
guard output == "4096" else {
throw IntegrationError.assert(
msg: "sysctl net.core.somaxconn should be '4096', got '\(output ?? "nil")'")
}
} catch {
try? await pod.stop()
throw error
}
}
func testPodSysctlMultipleContainers() async throws {
let id = "test-pod-sysctl-multi"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
// Containers in a pod share a network namespace, so use different
// sysctls per container to avoid clobbering.
let buffer1 = BufferWriter()
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.sysctl = [
"net.core.somaxconn": "2048"
]
config.process.arguments = ["cat", "/proc/sys/net/core/somaxconn"]
config.process.stdout = buffer1
}
let buffer2 = BufferWriter()
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.sysctl = [
"net.core.netdev_max_backlog": "5000"
]
config.process.arguments = ["cat", "/proc/sys/net/core/netdev_max_backlog"]
config.process.stdout = buffer2
}
do {
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 status \(status1) != 0")
}
let output1 = String(data: buffer1.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
guard output1 == "2048" else {
throw IntegrationError.assert(
msg: "container1 sysctl net.core.somaxconn should be '2048', got '\(output1 ?? "nil")'")
}
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 status \(status2) != 0")
}
let output2 = String(data: buffer2.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
guard output2 == "5000" else {
throw IntegrationError.assert(
msg: "container2 sysctl net.core.netdev_max_backlog should be '5000', got '\(output2 ?? "nil")'")
}
try await pod.stop()
} catch {
try? await pod.stop()
throw error
}
}
func testPodInvalidVolumeReference() async throws {
let id = "test-pod-invalid-volume-ref"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/true"]
config.mounts.append(.sharedMount(name: "nonexistent-volume", destination: "/data"))
}
do {
try await pod.create()
try? await pod.stop()
throw IntegrationError.assert(msg: "expected create() to fail for invalid volume reference")
} catch let error as ContainerizationError {
guard error.code == .invalidArgument else {
throw IntegrationError.assert(msg: "expected invalidArgument error, got: \(error)")
}
}
}
func testPodDuplicateVolumeName() async throws {
let id = "test-pod-duplicate-volume-name"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
config.volumes = [
.init(name: "data", source: .nbd(url: URL(string: "nbd://localhost:10809")!), format: "ext4"),
.init(name: "data", source: .nbd(url: URL(string: "nbd://localhost:10809")!), format: "ext4"),
]
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/true"]
}
do {
try await pod.create()
try? await pod.stop()
throw IntegrationError.assert(msg: "expected create() to fail for duplicate volume name")
} catch let error as ContainerizationError {
guard error.code == .invalidArgument else {
throw IntegrationError.assert(msg: "expected invalidArgument error, got: \(error)")
}
}
}
#if os(macOS)
@available(macOS 26.0, *)
func testPodIPv6AddressAdd() async throws {
let id = "test-pod-ipv6-address"
let bs = try await bootstrap(id)
var network = try VmnetNetwork(prefixV6: try CIDRv6("fd00::/64"))
defer {
try? network.releaseInterface(id)
}
guard let interface = try network.createInterface(id) else {
throw IntegrationError.assert(msg: "failed to create network interface")
}
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
config.interfaces = [interface]
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "100"]
}
try await pod.create()
try await pod.startContainer("container1")
let buffer = BufferWriter()
let exec = try await pod.execInContainer("container1", processID: "check-v6") { config in
config.arguments = ["ip", "-6", "addr", "show", "eth0"]
config.stdout = buffer
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
try await pod.killContainer("container1", signal: .kill)
try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "ip -6 addr show failed with status \(status)")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
}
guard output.contains("fd00::2") else {
throw IntegrationError.assert(
msg: "expected fd00::2 on eth0 inside pod container, got: \(output)")
}
}
#endif
func testPodFilesystemOperation() async throws {
let id = "test-pod-filesystem-operation"
let bs = try await bootstrap(id)
let diskImageURL = Self.testDir.appending(component: "\(id)-data.ext4")
try? FileManager.default.removeItem(at: diskImageURL)
let filesystem = try EXT4.Formatter(FilePath(diskImageURL.absolutePath()), minDiskSize: 64.mib())
try filesystem.close()
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["/bin/sleep", "1000"]
config.mounts.append(
Mount.block(
format: "ext4",
source: diskImageURL.absolutePath(),
destination: "/data"
))
}
do {
try await pod.create()
try await pod.startContainer("container1")
try await pod.filesystemOperation("container1", operation: .freeze, path: "/data")
let writeExec = try await pod.execInContainer("container1", processID: "write-hello") { config in
config.arguments = ["/bin/sh", "-c", "echo hello > /data/hello.txt"]
}
try await writeExec.start()
let writeStatus = try await writeExec.wait()
try await writeExec.delete()
guard writeStatus.exitCode == 0 else {
throw IntegrationError.assert(msg: "write exec failed with status \(writeStatus)")
}
try await pod.filesystemOperation("container1", operation: .thaw, path: "/data")
try await pod.filesystemOperation("container1", operation: .trim, path: "/data")
let readBuffer = BufferWriter()
let readExec = try await pod.execInContainer("container1", processID: "read-hello") { config in
config.arguments = ["/bin/cat", "/data/hello.txt"]
config.stdout = readBuffer
}
try await readExec.start()
let readStatus = try await readExec.wait()
try await readExec.delete()
guard readStatus.exitCode == 0 else {
throw IntegrationError.assert(msg: "read exec failed with status \(readStatus)")
}
let readOutput = String(decoding: readBuffer.data, as: UTF8.self)
guard readOutput == "hello\n" else {
throw IntegrationError.assert(
msg: "expected 'hello\\n' in /data/hello.txt, got: '\(readOutput)'"
)
}
try await pod.killContainer("container1", signal: .kill)
_ = try await pod.waitContainer("container1")
try await pod.stop()
} catch {
try? await pod.filesystemOperation("container1", operation: .thaw, path: "/data")
try? await pod.stop()
throw error
}
}
}