Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 70 additions & 31 deletions Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ public final class LinuxContainer: Container, Sendable {
public init() {}
}

private struct TemporaryMount {
let tempDestination: String
let destination: String

func toBindMount() -> ContainerizationOCI.Mount {
.init(
type: "bind",
source: tempDestination,
destination: destination,
options: ["bind"]
)
}
}

private let state: Mutex<State>

// Ports to be allocated from for stdio and for
Expand Down Expand Up @@ -166,69 +180,81 @@ public final class LinuxContainer: Container, Sendable {
struct CreatedState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
let tempMounts: [TemporaryMount]
}

struct StartingState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
let tempMounts: [TemporaryMount]

init(_ state: CreatedState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.tempMounts = state.tempMounts
}
}

struct PausingState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
let process: LinuxProcess
let tempMounts: [TemporaryMount]

init(_ state: StartedState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = state.process
self.tempMounts = state.tempMounts
}
}

struct PausedState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
let process: LinuxProcess
let tempMounts: [TemporaryMount]

init(_ state: PausingState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = state.process
self.tempMounts = state.tempMounts
}
}

struct ResumingState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
let process: LinuxProcess
let tempMounts: [TemporaryMount]

init(_ state: PausedState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = state.process
self.tempMounts = state.tempMounts
}
}

struct StartedState: Sendable {
let vm: any VirtualMachineInstance
let process: LinuxProcess
let relayManager: UnixSocketRelayManager
let tempMounts: [TemporaryMount]

init(_ state: StartingState, process: LinuxProcess) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = process
self.tempMounts = state.tempMounts
}

init(_ state: ResumingState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = state.process
self.tempMounts = state.tempMounts
}
}

Expand All @@ -254,11 +280,12 @@ public final class LinuxContainer: Container, Sendable {

mutating func setCreated(
vm: any VirtualMachineInstance,
relayManager: UnixSocketRelayManager
relayManager: UnixSocketRelayManager,
tempMounts: [TemporaryMount]
) throws {
switch self {
case .creating:
self = .created(.init(vm: vm, relayManager: relayManager))
self = .created(.init(vm: vm, relayManager: relayManager, tempMounts: tempMounts))
default:
throw ContainerizationError(
.invalidState,
Expand All @@ -267,11 +294,11 @@ public final class LinuxContainer: Container, Sendable {
}
}

mutating func setStarting() throws -> any VirtualMachineInstance {
mutating func setStarting() throws -> CreatedState {
switch self {
case .created(let state):
self = .starting(.init(state))
return state.vm
return state
default:
throw ContainerizationError(
.invalidState,
Expand Down Expand Up @@ -384,6 +411,8 @@ public final class LinuxContainer: Container, Sendable {
}
}

private static let guestContainerPath = "/run/container/"

private let vmm: VirtualMachineManager
private let logger: Logger?

Expand Down Expand Up @@ -457,7 +486,11 @@ public final class LinuxContainer: Container, Sendable {
}

private static func guestRootfsPath(_ id: String) -> String {
"/run/container/\(id)/rootfs"
Self.guestContainerPath + "\(id)/rootfs"
}

private func guestTempMountPath() -> String {
Self.guestContainerPath + "\(id)/temp-mounts"
}
}

Expand Down Expand Up @@ -500,13 +533,26 @@ extension LinuxContainer {
rootfs.destination = Self.guestRootfsPath(self.id)
try await agent.mount(rootfs)

// Start up our friendly unix socket relays.
// For unix domain socket mounts we'll create these at a temporary
// holding spot and then bind them into our container.
var temporaryMounts = [TemporaryMount]()
for socket in self.config.sockets {
var modifiedSocket = socket
let hashedMountSource = try hashMountSource(source: modifiedSocket.source.path)
let temporarySocketDestination = URL(filePath: guestTempMountPath()).appending(path: hashedMountSource)
modifiedSocket.destination = temporarySocketDestination

try await self.relayUnixSocket(
socket: socket,
socket: modifiedSocket,
relayManager: relayManager,
agent: agent
)

let tempMount = TemporaryMount(
tempDestination: temporarySocketDestination.path,
destination: socket.destination.path
)
temporaryMounts.append(tempMount)
}

// For every interface asked for:
Expand All @@ -530,7 +576,13 @@ extension LinuxContainer {
try await agent.configureHosts(config: hosts, location: rootfs.destination)
}

try self.state.withLock { try $0.setCreated(vm: vm, relayManager: relayManager) }
try self.state.withLock {
try $0.setCreated(
vm: vm,
relayManager: relayManager,
tempMounts: temporaryMounts
)
}
}
} catch {
try? await vm.stop()
Expand All @@ -541,13 +593,18 @@ extension LinuxContainer {

/// Start the container container's initial process.
public func start() async throws {
let vm = try self.state.withLock { try $0.setStarting() }
let createdState = try self.state.withLock { try $0.setStarting() }

let agent = try await vm.dialAgent()
let agent = try await createdState.vm.dialAgent()
do {
var spec = generateRuntimeSpec()
// We don't need the rootfs, nor do OCI runtimes want it included.
spec.mounts = vm.mounts.dropFirst().map { $0.to }
spec.mounts = createdState.vm.mounts.dropFirst().map { $0.to }

// Add in all of our temporary holding spot mounts.
for mount in createdState.tempMounts {
spec.mounts.append(mount.toBindMount())
}

let stdio = Self.setupIO(
portAllocator: self.hostVsockPorts,
Expand All @@ -562,7 +619,7 @@ extension LinuxContainer {
spec: spec,
io: stdio,
agent: agent,
vm: vm,
vm: createdState.vm,
logger: self.logger
)
try await process.start()
Expand Down Expand Up @@ -632,8 +689,6 @@ 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.sockets
if !sockets.isEmpty {
guard let relayAgent = agent as? SocketRelayAgent else {
Expand Down Expand Up @@ -781,20 +836,6 @@ extension LinuxContainer {
return try await state.process.closeStdin()
}

/// Relay a unix socket from in the container to the host, or from the host
/// to inside the container.
public func relayUnixSocket(socket: UnixSocketConfiguration) async throws {
let state = try self.state.withLock { try $0.startedState("relayUnixSocket") }

try await state.vm.withAgent { agent in
try await self.relayUnixSocket(
socket: socket,
relayManager: state.relayManager,
agent: agent
)
}
}

private func relayUnixSocket(
socket: UnixSocketConfiguration,
relayManager: UnixSocketRelayManager,
Expand All @@ -810,9 +851,7 @@ extension LinuxContainer {
var socket = socket
let rootInGuest = URL(filePath: self.root)

if socket.direction == .into {
socket.destination = rootInGuest.appending(path: socket.destination.path)
} else {
if socket.direction == .outOf {
socket.source = rootInGuest.appending(path: socket.source.path)
}

Expand Down
23 changes: 19 additions & 4 deletions Sources/ContainerizationOS/Mount/Mount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,27 @@ extension Mount {
// Ensure propagation type change flags aren't included in other calls.
let originalFlags = opts.flags & ~(propagationTypes)

let targetURL = URL(fileURLWithPath: self.target)
let targetURL = URL(fileURLWithPath: target)
let targetParent = targetURL.deletingLastPathComponent().path
if let perms = createWithPerms {
try mkdirAll(targetParent, perms)
let perms = createWithPerms ?? 0o755

// See if it's a bind mount, and if so and it's to a regular file
// only create up to the parent dir.
if opts.flags & MS_BIND == MS_BIND {
let fileInfo = try File.info(self.source)
if fileInfo.isRegularFile || fileInfo.isSocket {
try mkdirAll(targetParent, perms)
let fd = open(target, O_RDONLY | O_CREAT, 0o755)
if fd == -1 {
throw Error.errno(errno, "failed to create bind mount target at \(target)")
}
close(fd)
} else {
try mkdirAll(target, perms)
}
} else {
try mkdirAll(target, perms)
}
try mkdirAll(target, 0o755)

if opts.flags & MS_REMOUNT == 0 || !dataString.isEmpty {
guard _mount(self.source, target, self.type, UInt(originalFlags), dataString) == 0 else {
Expand Down