//===----------------------------------------------------------------------===// // 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 } } }