diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 9eb99f93..748162dd 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -27,9 +27,6 @@ import struct ContainerizationOS.Terminal /// `LinuxContainer` is an easy to use type for launching and managing the /// full lifecycle of a Linux container ran inside of a virtual machine. -/// -/// NOTE: Editing the properties of `LinuxContainer` after calling `start()` -/// have no effect. public final class LinuxContainer: Container, Sendable { /// The default PATH value for a process. public static let defaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" @@ -40,25 +37,114 @@ public final class LinuxContainer: Container, Sendable { /// Rootfs for the container. public let rootfs: Mount - private struct Configuration { - var spec: Spec - var cpus: Int = 4 - var memoryInBytes: UInt64 = 1024.mib() - var interfaces: [any Interface] = [] - var sockets: [UnixSocketConfiguration] = [] - var rosetta: Bool = false - var virtualization: Bool = false - var terminal: Terminal? = nil - var ioHandlers: LinuxProcess.IOHandler = .nullIO() - var mounts: [Mount] - var dns: DNS? = nil - var hosts: Hosts? = nil + /// Configuration for the container. + public let config: Configuration + + /// The configuration for the LinuxContainer. + public struct Configuration: Sendable { + /// Configuration of a container process. + public struct Process: Sendable { + /// The arguments for the container process. + public var arguments: [String] = [] + /// The environment variables for the container process. + public var environmentVariables: [String] = ["PATH=\(LinuxContainer.defaultPath)"] + /// The working directory for the container process. + public var workingDirectory: String = "/" + /// The user the container process will run as. + public var user: ContainerizationOCI.User = .init() + /// The rlimits for the container process. + public var rlimits: [POSIXRlimit] = [] + /// Whether to allocate a pseudo terminal for the process. If you'd like interactive + /// behavior and are planning to use a terminal for stdin/out/err on the client side, + /// this should likely be set to true. + public var terminal: Bool = false + /// The stdin for the process. + public var stdin: ReaderStream? + /// The stdout for the process. + public var stdout: Writer? + /// The stderr for the process. + public var stderr: Writer? + + public init() {} + + public init(from config: ImageConfig) { + self.workingDirectory = config.workingDir ?? "/" + self.environmentVariables = config.env ?? [] + self.arguments = (config.entrypoint ?? []) + (config.cmd ?? []) + self.user = { + if let rawString = config.user { + return User(username: rawString) + } + return User() + }() + } + + func toOCI() -> ContainerizationOCI.Process { + ContainerizationOCI.Process( + args: self.arguments, + cwd: self.workingDirectory, + env: self.environmentVariables, + user: self.user, + rlimits: self.rlimits, + terminal: self.terminal + ) + } + + /// Sets up IO to be handled by the passed in Terminal, and edits the + /// process configuration to set the necessary state for using a pty. + mutating public func setTerminalIO(terminal: Terminal) { + self.environmentVariables.append("TERM=xterm") + self.terminal = true + self.stdin = terminal + self.stdout = terminal + } + } + + /// Configuration for the init process of the container. + public var process = Process.init() + /// The amount of cpus for the container. + public var cpus: Int = 4 + /// The memory in bytes to give to the container. + public var memoryInBytes: UInt64 = 1024.mib() + /// The hostname for the container. + public var hostname: String = "" + /// The system control options for the container. + public var sysctl: [String: String] = [:] + /// The network interfaces for the container. + public var interfaces: [any Interface] = [] + /// The Unix domain socket relays to setup for the container. + public var sockets: [UnixSocketConfiguration] = [] + /// Whether rosetta x86-64 emulation should be setup for the container. + public var rosetta: Bool = false + /// Whether nested virtualization should be turned on for the container. + public var virtualization: Bool = false + /// The mounts for the container. + public var mounts: [Mount] = LinuxContainer.defaultMounts() + /// The DNS configuration for the container. + public var dns: DNS? + /// The hosts to add to /etc/hosts for the container. + public var hosts: Hosts? + + public init() {} + } + + /// `IOHandler` informs the container process about what should be done + /// for the stdio streams. + struct IOHandler: Sendable { + public var stdin: ReaderStream? + public var stdout: Writer? + public var stderr: Writer? + + init(stdin: ReaderStream? = nil, stdout: Writer? = nil, stderr: Writer? = nil) { + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + } } @SendablePropertyUnchecked private var state: State - private let config: Mutex // Ports to be allocated from for stdio and for // unix socket relays that are sharing a guest // uds to the host. @@ -228,28 +314,26 @@ public final class LinuxContainer: Container, Sendable { _ id: String, rootfs: Mount, vmm: VirtualMachineManager, - logger: Logger? = nil - ) { + logger: Logger? = nil, + configuration: (inout Configuration) throws -> Void + ) throws { self.id = id self.vmm = vmm self.hostVsockPorts = Atomic(0x1000_0000) self.guestVsockPorts = Atomic(0x1000_0000) self.rootfs = rootfs self.logger = logger - let configuration = Configuration( - spec: Self.createDefaultRuntimeSpec(id), - mounts: Self.createDefaultMounts() - ) - self.config = Mutex(configuration) + + var config = Configuration() + try configuration(&config) + + self.config = config self.state = .initialized } private static func createDefaultRuntimeSpec(_ id: String) -> Spec { .init( - process: .init( - cwd: "/", - env: ["PATH=\(Self.defaultPath)"] - ), + process: .init(), hostname: id, root: .init( path: Self.guestRootfsPath(id), @@ -261,11 +345,24 @@ public final class LinuxContainer: Container, Sendable { ) } - private static func guestRootfsPath(_ id: String) -> String { - "/run/container/\(id)/rootfs" + private func generateRuntimeSpec() -> Spec { + var spec = Self.createDefaultRuntimeSpec(id) + + // Process toggles. + spec.process = config.process.toOCI() + + // General toggles. + spec.hostname = config.hostname + + // Linux toggles. + var linux = ContainerizationOCI.Linux.init() + linux.sysctl = config.sysctl + spec.linux = linux + + return spec } - private static func createDefaultMounts() -> [Mount] { + public static func defaultMounts() -> [Mount] { let defaultOptions = ["nosuid", "noexec", "nodev"] return [ .any(type: "proc", source: "proc", destination: "/proc", options: defaultOptions), @@ -277,240 +374,31 @@ public final class LinuxContainer: Container, Sendable { .any(type: "devpts", source: "devpts", destination: "/dev/pts", options: ["nosuid", "noexec", "gid=5", "mode=620", "ptmxmode=666"]), ] } + + private static func guestRootfsPath(_ id: String) -> String { + "/run/container/\(id)/rootfs" + } } extension LinuxContainer { package var root: String { - config.withLock { $0.spec.root!.path } + Self.guestRootfsPath(id) } /// Number of CPU cores allocated. public var cpus: Int { - get { - config.withLock { $0.cpus } - } - set { - config.withLock { $0.cpus = newValue } - } + config.cpus } /// Amount of memory in bytes allocated for the container. /// This will be aligned to a 1MB boundary if it isn't already. public var memoryInBytes: UInt64 { - get { - config.withLock { $0.memoryInBytes } - } - set { - config.withLock { $0.memoryInBytes = newValue } - } + config.memoryInBytes } /// Network interfaces of the container. public var interfaces: [any Interface] { - get { - config.withLock { $0.interfaces } - } - set { - config.withLock { $0.interfaces = newValue } - } - } - - /// DNS configuration for the container. - public var dns: DNS? { - get { - config.withLock { $0.dns } - } - set { - config.withLock { $0.dns = newValue } - } - } - - /// Hostname mapping configurations for the container. - public var hosts: Hosts? { - get { config.withLock { $0.hosts } } - set { config.withLock { $0.hosts = newValue } } - } - - /// Unix sockets to share into or out of the container. - /// - /// The VirtualMachineAgent used to launch the container - /// MUST conform to `SocketRelayAgent` to support this; otherwise, - /// a ContainerizationError will be returned on start with the code - /// set to `.unsupported`. - public var sockets: [UnixSocketConfiguration] { - get { - config.withLock { $0.sockets } - } - set { - config.withLock { $0.sockets = newValue } - } - } - - /// Enable/disable x86-64 emulation in the container. - public var rosetta: Bool { - get { - config.withLock { $0.rosetta } - } - set { - config.withLock { $0.rosetta = newValue } - } - } - - /// Enable/disable virtualization capabilities in the container. - public var virtualization: Bool { - get { - config.withLock { $0.virtualization } - } - set { - config.withLock { $0.virtualization = newValue } - } - } - - /// Filesystem mounts for the container. - public var mounts: [Mount] { - get { - config.withLock { $0.mounts } - } - set { - config.withLock { $0.mounts = newValue } - } - } - - /// Arguments passed to the container. - public var arguments: [String] { - get { - config.withLock { $0.spec.process!.args } - } - set { - config.withLock { $0.spec.process!.args = newValue } - } - } - - /// Environment variables for the container. - public var environment: [String] { - get { - config.withLock { $0.spec.process!.env } - } - set { - config.withLock { $0.spec.process!.env = newValue } - } - } - - /// The current working directory (cwd) for the container. - public var workingDirectory: String { - get { - config.withLock { $0.spec.process!.cwd } - } - set { - config.withLock { $0.spec.process!.cwd = newValue } - } - } - - /// The User the container should execute under. - public var user: ContainerizationOCI.User { - get { - config.withLock { $0.spec.process!.user } - } - set { - config.withLock { $0.spec.process!.user = newValue } - } - } - - /// Set the hostname for the container. - public var hostname: String { - get { - config.withLock { $0.spec.hostname } - } - set { - config.withLock { $0.spec.hostname = newValue } - } - } - - /// Set any sysctls for the container's environment. - public var sysctl: [String: String]? { - get { - config.withLock { $0.spec.linux!.sysctl } - } - set { - config.withLock { $0.spec.linux!.sysctl = newValue } - } - } - - /// Rlimits for the container. - public var rlimits: [POSIXRlimit] { - get { - config.withLock { $0.spec.process!.rlimits } - } - set { - config.withLock { $0.spec.process!.rlimits = newValue } - } - } - - /// Set a pty device as the container's stdio. This additionally will - /// set the TERM=xterm environment variable, and the OCI runtime specs - /// `process.terminal` field to true. - public var terminalDevice: Terminal? { - get { - config.withLock { $0.terminal } - } - set { - config.withLock { - $0.spec.process!.terminal = newValue != nil ? true : false - $0.terminal = newValue - $0.spec.process!.env.append("TERM=xterm") - $0.ioHandlers.stdin = newValue - $0.ioHandlers.stdout = newValue - $0.ioHandlers.stderr = nil - } - } - } - - /// If the container has a pty allocated. - public var terminal: Bool { - get { - config.withLock { $0.spec.process!.terminal } - } - set { - config.withLock { - $0.spec.process!.terminal = newValue - $0.spec.process!.env.append("TERM=xterm") - } - } - } - - /// Set the stdin stream for the initial process of the container. - public var stdin: ReaderStream? { - get { - config.withLock { $0.ioHandlers.stdin } - } - set { - config.withLock { $0.ioHandlers.stdin = newValue } - } - } - - /// Set the stdout handler for the initial process of the container. - public var stdout: Writer? { - get { - config.withLock { $0.ioHandlers.stdout } - } - set { - config.withLock { $0.ioHandlers.stdout = newValue } - } - } - - /// Set the stderr handler for the initial process of the container. - public var stderr: Writer? { - get { - config.withLock { $0.ioHandlers.stderr } - } - set { - config.withLock { $0.ioHandlers.stderr = newValue } - } - } - - public func setProcessConfig(from imageConfig: ImageConfig) { - let process = ContainerizationOCI.Process(from: imageConfig) - self.config.withLock { $0.spec.process = process } + config.interfaces } /// Create the underlying container's virtual machine @@ -532,7 +420,7 @@ extension LinuxContainer { try await agent.mount(rootfs) // Start up our friendly unix socket relays. - for socket in self.sockets { + for socket in self.config.sockets { try await self.relayUnixSocket( socket: socket, relayManager: relayManager, @@ -554,10 +442,10 @@ extension LinuxContainer { } // Setup /etc/resolv.conf and /etc/hosts if asked for. - if let dns = self.dns { + if let dns = self.config.dns { try await agent.configureDNS(config: dns, location: rootfs.destination) } - if let hosts = self.hosts { + if let hosts = self.config.hosts { try await agent.configureHosts(config: hosts, location: rootfs.destination) } @@ -577,21 +465,21 @@ extension LinuxContainer { let agent = try await vm.dialAgent() do { - var specCopy = config.withLock { $0.spec } + var spec = generateRuntimeSpec() // We don't need the rootfs, nor do OCI runtimes want it included. - specCopy.mounts = vm.mounts.dropFirst().map { $0.to } + spec.mounts = vm.mounts.dropFirst().map { $0.to } let stdio = Self.setupIO( portAllocator: self.hostVsockPorts, - stdin: self.stdin, - stdout: self.stdout, - stderr: self.stderr + stdin: self.config.process.stdin, + stdout: self.config.process.stdout, + stderr: self.config.process.stderr ) let process = LinuxProcess( self.id, containerID: self.id, - spec: specCopy, + spec: spec, io: stdio, agent: agent, vm: vm, @@ -667,7 +555,7 @@ extension LinuxContainer { try await startedState.vm.withAgent { agent in // First, we need to stop any unix socket relays as this will // keep the rootfs from being able to umount (EBUSY). - let sockets = config.withLock { $0.sockets } + let sockets = config.sockets if !sockets.isEmpty { guard let relayAgent = agent as? SocketRelayAgent else { throw ContainerizationError( @@ -726,29 +614,51 @@ extension LinuxContainer { } /// Execute a new process in the container. - public func exec( - _ id: String, - configuration: ContainerizationOCI.Process, - stdin: ReaderStream? = nil, - stdout: Writer? = nil, - stderr: Writer? = nil - ) async throws -> LinuxProcess { + public func exec(_ id: String, configuration: (inout Configuration.Process) throws -> Void) async throws -> LinuxProcess { let state = try self.state.startedState("exec") - var specCopy = config.withLock { $0.spec } - specCopy.process = configuration + var spec = generateRuntimeSpec() + var config = Configuration.Process() + try configuration(&config) + spec.process = config.toOCI() let stdio = Self.setupIO( portAllocator: self.hostVsockPorts, - stdin: stdin, - stdout: stdout, - stderr: stderr + stdin: config.stdin, + stdout: config.stdout, + stderr: config.stderr ) let agent = try await state.vm.dialAgent() let process = LinuxProcess( id, containerID: self.id, - spec: specCopy, + spec: spec, + io: stdio, + agent: agent, + vm: state.vm, + logger: self.logger + ) + return process + } + + /// Execute a new process in the container. + public func exec(_ id: String, configuration: Configuration.Process) async throws -> LinuxProcess { + let state = try self.state.startedState("exec") + + var spec = generateRuntimeSpec() + spec.process = configuration.toOCI() + + let stdio = Self.setupIO( + portAllocator: self.hostVsockPorts, + stdin: configuration.stdin, + stdout: configuration.stdout, + stderr: configuration.stderr + ) + let agent = try await state.vm.dialAgent() + let process = LinuxProcess( + id, + containerID: self.id, + spec: spec, io: stdio, agent: agent, vm: state.vm, diff --git a/Sources/Containerization/LinuxProcess.swift b/Sources/Containerization/LinuxProcess.swift index ea574f50..236e080a 100644 --- a/Sources/Containerization/LinuxProcess.swift +++ b/Sources/Containerization/LinuxProcess.swift @@ -25,24 +25,6 @@ import Synchronization /// `LinuxProcess` represents a Linux process and is used to /// setup and control the full lifecycle for the process. public final class LinuxProcess: Sendable { - /// `IOHandler` informs the process about what should be done - /// for the stdio streams. - public struct IOHandler: Sendable { - public var stdin: ReaderStream? - public var stdout: Writer? - public var stderr: Writer? - - public init(stdin: ReaderStream? = nil, stdout: Writer? = nil, stderr: Writer? = nil) { - self.stdin = stdin - self.stdout = stdout - self.stderr = stderr - } - - public static func nullIO() -> IOHandler { - .init() - } - } - /// The ID of the process. This is purely metadata for the caller. public let id: String @@ -109,52 +91,6 @@ public final class LinuxProcess: Sendable { state.withLock { $0.pid } } - /// Arguments passed to the Process. - public var arguments: [String] { - get { - state.withLock { $0.spec.process!.args } - } - set { - state.withLock { $0.spec.process!.args = newValue } - } - } - - /// Environment variables for the Process. - public var environment: [String] { - get { state.withLock { $0.spec.process!.env } } - set { state.withLock { $0.spec.process!.env = newValue } } - } - - /// The current working directory (cwd) for the Process. - public var workingDirectory: String { - get { state.withLock { $0.spec.process!.cwd } } - set { state.withLock { $0.spec.process!.cwd = newValue } } - } - - /// A boolean value indicating if a Terminal or PTY device should - /// be attached to the Process's Standard I/O. - public var terminal: Bool { - get { state.withLock { $0.spec.process!.terminal } } - set { - state.withLock { - $0.spec.process!.terminal = newValue - $0.spec.process!.env.append("TERM=xterm") - } - } - } - - /// The User a Process should execute under. - public var user: ContainerizationOCI.User { - get { state.withLock { $0.spec.process!.user } } - set { state.withLock { $0.spec.process!.user = newValue } } - } - - /// Rlimits for the Process. - public var rlimits: [POSIXRlimit] { - get { state.withLock { $0.spec.process!.rlimits } } - set { state.withLock { $0.spec.process!.rlimits = newValue } } - } - private let state: Mutex private let ioSetup: Stdio private let agent: any VirtualMachineAgent diff --git a/Sources/Containerization/VZVirtualMachineManager.swift b/Sources/Containerization/VZVirtualMachineManager.swift index c6a546f5..f2fa38f1 100644 --- a/Sources/Containerization/VZVirtualMachineManager.swift +++ b/Sources/Containerization/VZVirtualMachineManager.swift @@ -60,10 +60,10 @@ public struct VZVirtualMachineManager: VirtualMachineManager { if let bootlog { config.bootlog = URL(filePath: bootlog) } - config.rosetta = c.rosetta - config.nestedVirtualization = c.virtualization + config.rosetta = c.config.rosetta + config.nestedVirtualization = c.config.virtualization - config.mounts = [c.rootfs] + c.mounts + config.mounts = [c.rootfs] + c.config.mounts }) } } diff --git a/Sources/Integration/ProcessTests.swift b/Sources/Integration/ProcessTests.swift index f863190c..b0dd9f9b 100644 --- a/Sources/Integration/ProcessTests.swift +++ b/Sources/Integration/ProcessTests.swift @@ -27,12 +27,9 @@ extension IntegrationSuite { let id = "test-process-true" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/bin/true"] + 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() @@ -49,8 +46,9 @@ extension IntegrationSuite { let id = "test-process-false" let bs = try await bootstrap() - let container = LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) - container.arguments = ["/bin/false"] + 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() @@ -92,11 +90,12 @@ extension IntegrationSuite { func testProcessEchoHi() async throws { let id = "test-process-echo-hi" let bs = try await bootstrap() - let container = LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) - container.arguments = ["/bin/echo", "hi"] let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -123,28 +122,19 @@ extension IntegrationSuite { let id = "test-concurrent-processes" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/bin/sleep", "1000"] + 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 execConfig = ContainerizationOCI.Process( - args: ["/bin/true"], - env: ["PATH=\(LinuxContainer.defaultPath)"] - ) - try await withThrowingTaskGroup(of: Void.self) { group in for i in 0...80 { - let exec = try await container.exec( - "exec-\(i)", - configuration: execConfig - ) + let exec = try await container.exec("exec-\(i)") { config in + config.arguments = ["/bin/true"] + } group.addTask { try await exec.start() @@ -174,54 +164,48 @@ extension IntegrationSuite { func testMultipleConcurrentProcessesOutputStress() async throws { let id = "test-concurrent-processes-output-stress" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/bin/sleep", "1000"] + 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 baseExecConfig = ContainerizationOCI.Process( - args: ["sh", "-c", "dd if=/dev/random of=/tmp/bytes bs=1M count=20 status=none ; sha256sum /tmp/bytes"], - env: ["PATH=\(LinuxContainer.defaultPath)"] - ) let buffer = BufferWriter() - let exec = try await container.exec( - "expected-value", - configuration: baseExecConfig, - stdout: buffer, - ) + 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 - let execConfig = ContainerizationOCI.Process( - args: ["cat", "/tmp/bytes"], - env: ["PATH=\(LinuxContainer.defaultPath)"] - ) for i in 0...80 { let idx = i group.addTask { let buffer = BufferWriter() - let exec = try await container.exec( - "exec-\(idx)", - configuration: execConfig, - stdout: buffer, - ) + 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 @@ -249,16 +233,12 @@ extension IntegrationSuite { let id = "test-process-user" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/usr/bin/id"] - container.user = .init(uid: 1, gid: 1, additionalGids: [1]) - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -282,16 +262,12 @@ extension IntegrationSuite { let id = "test-process-tty-envvar" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["env"] - container.terminal = true - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -320,16 +296,12 @@ extension IntegrationSuite { let id = "test-process-home-envvar" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["env"] - container.user = .init(uid: 0, gid: 0) - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -357,15 +329,14 @@ extension IntegrationSuite { let id = "test-process-custom-home-envvar" let bs = try await bootstrap() - let container = LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) - let customHomeEnvvar = "HOME=/tmp/custom/home" - container.environment = [customHomeEnvvar] - container.arguments = ["sh", "-c", "echo HOME=$HOME"] - container.user = .init(uid: 0, gid: 0) - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -390,16 +361,12 @@ extension IntegrationSuite { let id = "test-container-hostname" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/bin/hostname"] - container.hostname = "foo-bar" - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -422,17 +389,13 @@ extension IntegrationSuite { let id = "test-container-hosts-file" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["cat", "/etc/hosts"] let entry = Hosts.Entry.localHostIPV4(comment: "Testaroo") - container.hosts = Hosts(entries: [entry]) - let buffer = BufferWriter() - container.stdout = buffer + 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() @@ -455,16 +418,12 @@ extension IntegrationSuite { let id = "test-container-stdin" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["cat"] - container.stdin = StdinBuffer(data: "Hello from test".data(using: .utf8)!) - let buffer = BufferWriter() - container.stdout = buffer + 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() diff --git a/Sources/Integration/VMTests.swift b/Sources/Integration/VMTests.swift index 4c1dee7a..6c5ff93d 100644 --- a/Sources/Integration/VMTests.swift +++ b/Sources/Integration/VMTests.swift @@ -26,17 +26,13 @@ extension IntegrationSuite { let id = "test-cat-mount" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - let directory = try createMountDirectory() - container.mounts.append(.share(source: directory.path, destination: "/mnt")) - container.arguments = ["/bin/cat", "/mnt/hi.txt"] - let buffer = BufferWriter() - container.stdout = buffer + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + let directory = try createMountDirectory() + config.process.arguments = ["/bin/cat", "/mnt/hi.txt"] + config.mounts.append(.share(source: directory.path, destination: "/mnt")) + config.process.stdout = buffer + } try await container.create() try await container.start() @@ -60,14 +56,10 @@ extension IntegrationSuite { let id = "test-nested-virt" let bs = try await bootstrap() - let container = LinuxContainer( - id, - rootfs: bs.rootfs, - vmm: bs.vmm - ) - container.arguments = ["/bin/true"] - - container.virtualization = true + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["/bin/true"] + config.virtualization = true + } do { try await container.create() diff --git a/Sources/cctl/ContainerStore.swift b/Sources/cctl/ContainerStore.swift index a3b85eb8..0c4f1ce2 100644 --- a/Sources/cctl/ContainerStore.swift +++ b/Sources/cctl/ContainerStore.swift @@ -71,7 +71,12 @@ struct ContainerStore: Sendable { } } - public func create(id: String, reference: String, fsSizeInBytes: UInt64) async throws -> LinuxContainer { + public func create( + id: String, + reference: String, + fsSizeInBytes: UInt64, + configuration: (inout LinuxContainer.Configuration) throws -> Void + ) async throws -> LinuxContainer { let initImage = try await image.getInitImage(reference: Self.initImage) let initfs = try await { do { @@ -119,14 +124,12 @@ struct ContainerStore: Sendable { bootlog: "cctl.log" ) - let linuxContainer = LinuxContainer( - id, - rootfs: imageBlock, - vmm: vmm - ) - if let imageConfig { - linuxContainer.setProcessConfig(from: imageConfig) + return try LinuxContainer(id, rootfs: imageBlock, vmm: vmm) { config in + if let imageConfig { + let process = LinuxContainer.Configuration.Process(from: imageConfig) + config.process = process + } + try configuration(&config) } - return linuxContainer } } diff --git a/Sources/cctl/RunCommand.swift b/Sources/cctl/RunCommand.swift index fe424869..6d725bbd 100644 --- a/Sources/cctl/RunCommand.swift +++ b/Sources/cctl/RunCommand.swift @@ -94,48 +94,48 @@ extension Application { id: id, reference: imageReference, fsSizeInBytes: fsSizeInMB.mib() - ) - container.cpus = cpus - container.memoryInBytes = memory.mib() + ) { config in + config.cpus = cpus + config.memoryInBytes = memory.mib() + config.process.setTerminalIO(terminal: current) + config.process.arguments = arguments + config.process.workingDirectory = cwd - container.terminalDevice = current - container.arguments = arguments - container.workingDirectory = cwd - - for mount in self.mounts { - let paths = mount.split(separator: ":") - if paths.count != 2 { - throw ContainerizationError( - .invalidArgument, - message: "incorrect mount format detected: \(mount)" + for mount in self.mounts { + let paths = mount.split(separator: ":") + if paths.count != 2 { + throw ContainerizationError( + .invalidArgument, + message: "incorrect mount format detected: \(mount)" + ) + } + let host = String(paths[0]) + let guest = String(paths[1]) + let czMount = Containerization.Mount.share( + source: host, + destination: guest ) + config.mounts.append(czMount) } - let host = String(paths[0]) - let guest = String(paths[1]) - let czMount = Containerization.Mount.share( - source: host, - destination: guest - ) - container.mounts.append(czMount) - } - var hosts = Hosts.default - if let ip { - guard let gateway else { - throw ContainerizationError(.invalidArgument, message: "gateway must be specified") + var hosts = Hosts.default + if let ip { + guard let gateway else { + throw ContainerizationError(.invalidArgument, message: "gateway must be specified") + } + config.interfaces.append(NATInterface(address: ip, gateway: gateway)) + config.dns = .init(nameservers: [gateway]) + if nameservers.count > 0 { + config.dns = .init(nameservers: nameservers) + } + hosts.entries.append( + Hosts.Entry( + ipAddress: ip, + hostnames: [id] + )) } - container.interfaces.append(NATInterface(address: ip, gateway: gateway)) - container.dns = .init(nameservers: [gateway]) - if nameservers.count > 0 { - container.dns = .init(nameservers: nameservers) - } - hosts.entries.append( - Hosts.Entry( - ipAddress: ip, - hostnames: [id] - )) + config.hosts = hosts } - container.hosts = hosts try await container.create() try await container.start()