mirror of
https://github.com/apple/container.git
synced 2026-09-21 23:25:43 +00:00
LinuxContainer/LinuxProcess: Rework supplying configuration (#219)
This is a fairly large reworking, but it gets rid of something that has plagued this since release which is the properties needing to be locked to be Sendable compliant. This was somewhat of a copout because we mostly know there's not a great deal of ways to have misused the setup today, but alas we'd need to either mark the type as `@unchecked` or just find a different route for setting the configuration. This change: Exposes the underlying Configuration type that today only housed things that aren't on the OCI spec. I'd love to just expose the OCI spec, but we don't (and possibly never will) support everything on the spec, so exposing it to be freely modified would be a bit odd. Now everything related to the container is configured on this type, and the same goes for execs.
This commit is contained in:
@@ -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<Configuration>
|
||||
// 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<UInt32>(0x1000_0000)
|
||||
self.guestVsockPorts = Atomic<UInt32>(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,
|
||||
|
||||
@@ -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<State>
|
||||
private let ioSetup: Stdio
|
||||
private let agent: any VirtualMachineAgent
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user