//===----------------------------------------------------------------------===// // Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import ArgumentParser import Containerization import ContainerizationOCI import ContainerizationOS import Crypto import Foundation import Logging extension IntegrationSuite { func testProcessTrue() async throws { let id = "test-process-true" let bs = try await bootstrap() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/true"] } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } } func testProcessFalse() async throws { let id = "test-process-false" let bs = try await bootstrap() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/false"] } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 1 else { throw IntegrationError.assert(msg: "process status \(status) != 1") } } final class BufferWriter: Writer { nonisolated(unsafe) var data = Data() func write(_ data: Data) throws { guard data.count > 0 else { return } self.data.append(data) } } final class StdinBuffer: ReaderStream { let data: Data init(data: Data) { self.data = data } func stream() -> AsyncStream { let (stream, cont) = AsyncStream.makeStream() cont.yield(self.data) cont.finish() return stream } } func testProcessEchoHi() async throws { let id = "test-process-echo-hi" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/echo", "hi"] config.process.stdout = buffer } do { try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 1") } guard String(data: buffer.data, encoding: .utf8) == "hi\n" else { throw IntegrationError.assert( msg: "process should have returned on stdout 'hi' != '\(String(data: buffer.data, encoding: .utf8)!)'") } } catch { try? await container.stop() throw error } } func testMultipleConcurrentProcesses() async throws { let id = "test-concurrent-processes" let bs = try await bootstrap() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/sleep", "1000"] } do { try await container.create() try await container.start() try await withThrowingTaskGroup(of: Void.self) { group in for i in 0...80 { let exec = try await container.exec("exec-\(i)") { config in config.arguments = ["/bin/true"] } group.addTask { try await exec.start() let status = try await exec.wait() if status != 0 { throw IntegrationError.assert(msg: "process status \(status) != 0") } try await exec.delete() } } // wait for all the exec'd processes. try await group.waitForAll() print("all group processes exit") // kill the init process. try await container.kill(SIGKILL) let status = try await container.wait() try await container.stop() print("\(status)") } } catch { throw error } } func testMultipleConcurrentProcessesOutputStress() async throws { let id = "test-concurrent-processes-output-stress" let bs = try await bootstrap() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/sleep", "1000"] } do { try await container.create() try await container.start() let buffer = BufferWriter() let exec = try await container.exec("expected-value") { config in config.arguments = [ "sh", "-c", "dd if=/dev/random of=/tmp/bytes bs=1M count=20 status=none ; sha256sum /tmp/bytes", ] config.stdout = buffer } try await exec.start() let status = try await exec.wait() if status != 0 { throw IntegrationError.assert(msg: "process status \(status) != 0") } let output = String(data: buffer.data, encoding: .utf8)! let expected = String(output.split(separator: " ").first!) try await withThrowingTaskGroup(of: Void.self) { group in for i in 0...80 { let idx = i group.addTask { let buffer = BufferWriter() let exec = try await container.exec("exec-\(idx)") { config in config.arguments = ["cat", "/tmp/bytes"] config.stdout = buffer } try await exec.start() let status = try await exec.wait() if status != 0 { throw IntegrationError.assert(msg: "process \(idx) status \(status) != 0") } var hasher = SHA256() hasher.update(data: buffer.data) let hash = hasher.finalize().digestString.trimmingDigestPrefix guard hash == expected else { throw IntegrationError.assert( msg: "process \(idx) output \(hash) != expected \(expected)") } try await exec.delete() } } // wait for all the exec'd processes. try await group.waitForAll() print("all group processes exit") // kill the init process. try await container.kill(SIGKILL) try await container.wait() try await container.stop() } } } func testProcessUser() async throws { let id = "test-process-user" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/usr/bin/id"] config.process.user = .init(uid: 1, gid: 1, additionalGids: [1]) config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } let expected = "uid=1(bin) gid=1(bin) groups=1(bin)" guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else { throw IntegrationError.assert( msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'") } } // Ensure if we ask for a terminal we set TERM. func testProcessTtyEnvvar() async throws { let id = "test-process-tty-envvar" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["env"] config.process.terminal = true config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } guard let str = String(data: buffer.data, encoding: .utf8) else { throw IntegrationError.assert( msg: "failed to convert standard output to a UTF8 string") } let homeEnvvar = "TERM=xterm" guard str.contains(homeEnvvar) else { throw IntegrationError.assert( msg: "process should have TERM environment variable defined") } } // Make sure we set HOME by default if we can find it in /etc/passwd in the guest. func testProcessHomeEnvvar() async throws { let id = "test-process-home-envvar" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["env"] config.process.user = .init(uid: 0, gid: 0) config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } guard let str = String(data: buffer.data, encoding: .utf8) else { throw IntegrationError.assert( msg: "failed to convert standard output to a UTF8 string") } let homeEnvvar = "HOME=/root" guard str.contains(homeEnvvar) else { throw IntegrationError.assert( msg: "process should have HOME environment variable defined") } } func testProcessCustomHomeEnvvar() async throws { let id = "test-process-custom-home-envvar" let bs = try await bootstrap() let customHomeEnvvar = "HOME=/tmp/custom/home" let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["sh", "-c", "echo HOME=$HOME"] config.process.environmentVariables.append(customHomeEnvvar) config.process.user = .init(uid: 0, gid: 0) config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 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 stdout to UTF8") } guard output.contains(customHomeEnvvar) else { throw IntegrationError.assert(msg: "process should have preserved custom HOME environment variable, expected \(customHomeEnvvar), got: \(output)") } } func testHostname() async throws { let id = "test-container-hostname" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["/bin/hostname"] config.hostname = "foo-bar" config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } let expected = "foo-bar" guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else { throw IntegrationError.assert( msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'") } } func testHostsFile() async throws { let id = "test-container-hosts-file" let bs = try await bootstrap() let entry = Hosts.Entry.localHostIPV4(comment: "Testaroo") let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["cat", "/etc/hosts"] config.hosts = Hosts(entries: [entry]) config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } let expected = entry.rendered guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else { throw IntegrationError.assert( msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'") } } func testProcessStdin() async throws { let id = "test-container-stdin" let bs = try await bootstrap() let buffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["cat"] config.process.stdin = StdinBuffer(data: "Hello from test".data(using: .utf8)!) config.process.stdout = buffer } try await container.create() try await container.start() let status = try await container.wait() try await container.stop() guard status == 0 else { throw IntegrationError.assert(msg: "process status \(status) != 0") } let expected = "Hello from test" guard String(data: buffer.data, encoding: .utf8) == "\(expected)" else { throw IntegrationError.assert( msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'") } } }