diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index eaa8707e..789310d7 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. 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" - /// The identifier of the container. public let id: String @@ -41,66 +38,8 @@ public final class LinuxContainer: Container, Sendable { /// 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() + public var process = LinuxProcessConfiguration.init() /// The amount of cpus for the container. public var cpus: Int = 4 /// The memory in bytes to give to the container. @@ -615,12 +554,12 @@ extension LinuxContainer { /// Execute a new process in the container. The process is not started after this call, and must be manually started /// via the `start` method. - public func exec(_ id: String, configuration: @Sendable @escaping (inout Configuration.Process) throws -> Void) async throws -> LinuxProcess { + public func exec(_ id: String, configuration: @Sendable @escaping (inout LinuxProcessConfiguration) throws -> Void) async throws -> LinuxProcess { try await self.state.withLock { let state = try $0.startedState("exec") var spec = self.generateRuntimeSpec() - var config = Configuration.Process() + var config = LinuxProcessConfiguration() try configuration(&config) spec.process = config.toOCI() @@ -646,7 +585,7 @@ extension LinuxContainer { /// Execute a new process in the container. The process is not started after this call, and must be manually started /// via the `start` method. - public func exec(_ id: String, configuration: Configuration.Process) async throws -> LinuxProcess { + public func exec(_ id: String, configuration: LinuxProcessConfiguration) async throws -> LinuxProcess { try await self.state.withLock { let state = try $0.startedState("exec") diff --git a/Sources/Containerization/LinuxProcessConfiguration.swift b/Sources/Containerization/LinuxProcessConfiguration.swift new file mode 100644 index 00000000..8c749a06 --- /dev/null +++ b/Sources/Containerization/LinuxProcessConfiguration.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 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 ContainerizationOCI +import ContainerizationOS + +public struct LinuxProcessConfiguration: Sendable { + /// The default PATH value for a process. + public static let defaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + + /// The arguments for the container process. + public var arguments: [String] = [] + /// The environment variables for the container process. + public var environmentVariables: [String] = ["PATH=\(Self.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( + arguments: [String], + environmentVariables: [String] = ["PATH=\(Self.defaultPath)"], + workingDirectory: String = "/", + user: ContainerizationOCI.User = .init(), + rlimits: [POSIXRlimit] = [], + terminal: Bool = false, + stdin: ReaderStream? = nil, + stdout: Writer? = nil, + stderr: Writer? = nil + ) { + self.arguments = arguments + self.environmentVariables = environmentVariables + self.workingDirectory = workingDirectory + self.user = user + self.rlimits = rlimits + self.terminal = terminal + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + } + + 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() + }() + } + + /// 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 + } + + func toOCI() -> ContainerizationOCI.Process { + ContainerizationOCI.Process( + args: self.arguments, + cwd: self.workingDirectory, + env: self.environmentVariables, + user: self.user, + rlimits: self.rlimits, + terminal: self.terminal + ) + } +} diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index f1d1f05b..5b8f1b0e 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -29,8 +29,6 @@ public struct Vminitd: Sendable { // Default vsock port that the agent and client use. public static let port: UInt32 = 1024 - private static let defaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - let client: Client public init(client: Client) { @@ -53,7 +51,7 @@ extension Vminitd: VirtualMachineAgent { public func standardSetup() async throws { try await up(name: "lo") - try await setenv(key: "PATH", value: Self.defaultPath) + try await setenv(key: "PATH", value: LinuxProcessConfiguration.defaultPath) let mounts: [ContainerizationOCI.Mount] = [ .init(type: "sysfs", source: "sysfs", destination: "/sys"), diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index e65104f1..e607b6df 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -31,7 +31,7 @@ struct LinuxContainerTests { workingDir: "/app" ) - let process = LinuxContainer.Configuration.Process(from: imageConfig) + let process = LinuxProcessConfiguration(from: imageConfig) #expect(process.workingDirectory == "/app") #expect(process.environmentVariables == ["NODE_ENV=production", "PORT=3000"]) @@ -48,7 +48,7 @@ struct LinuxContainerTests { workingDir: nil ) - let process = LinuxContainer.Configuration.Process(from: imageConfig) + let process = LinuxProcessConfiguration(from: imageConfig) #expect(process.workingDirectory == "/") #expect(process.environmentVariables == []) @@ -62,7 +62,7 @@ struct LinuxContainerTests { cmd: ["echo 'hello'", "&&", "sleep 10"] ) - let process = LinuxContainer.Configuration.Process(from: imageConfig) + let process = LinuxProcessConfiguration(from: imageConfig) #expect(process.arguments == ["/bin/sh", "-c", "echo 'hello'", "&&", "sleep 10"]) }