Skip to content

Commit 056a909

Browse files
authored
VirtualMachineManager: Rework protocol (#341)
Today this protocols create method is just odd. Our only implementation of it immediately casts to LinuxContainer, failing if it cannot do so. I think what might make more sense is to pass in a configuration itself with core parameters that we expect every vmm to be able to support, and then in a specific implementation they can continue to cast this type to a specific one to possibly extract some extra configuration values (rosetta for VZ for example). This rework will also make it simpler to support a Pod type, as the vm setup is identical and simple.
1 parent 4f996d3 commit 056a909

File tree

8 files changed

+164
-53
lines changed

8 files changed

+164
-53
lines changed

Sources/Containerization/ContainerManager.swift

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,30 +197,38 @@ public struct ContainerManager: Sendable {
197197
}
198198

199199
/// Create a new manager with the provided kernel, initfs mount, image store
200-
/// and optional network implementation.
200+
/// and optional network implementation. This will use a Virtualization.framework
201+
/// backed VMM implicitly.
201202
public init(
202203
kernel: Kernel,
203204
initfs: Mount,
204205
imageStore: ImageStore,
205-
network: Network? = nil
206+
network: Network? = nil,
207+
rosetta: Bool = false,
208+
nestedVirtualization: Bool = false
206209
) throws {
207210
self.imageStore = imageStore
208211
self.network = network
209212
try Self.createRootDirectory(path: self.imageStore.path)
210213
self.vmm = VZVirtualMachineManager(
211214
kernel: kernel,
212215
initialFilesystem: initfs,
213-
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
216+
bootlog: imageStore.path.appendingPathComponent("bootlog.log").absolutePath(),
217+
rosetta: rosetta,
218+
nestedVirtualization: nestedVirtualization
214219
)
215220
}
216221

217222
/// Create a new manager with the provided kernel, initfs mount, root state
218-
/// directory and optional network implementation.
223+
/// directory and optional network implementation. This will use a Virtualization.framework
224+
/// backed VMM implicitly.
219225
public init(
220226
kernel: Kernel,
221227
initfs: Mount,
222228
root: URL? = nil,
223-
network: Network? = nil
229+
network: Network? = nil,
230+
rosetta: Bool = false,
231+
nestedVirtualization: Bool = false
224232
) throws {
225233
if let root {
226234
self.imageStore = try ImageStore(path: root)
@@ -232,17 +240,22 @@ public struct ContainerManager: Sendable {
232240
self.vmm = VZVirtualMachineManager(
233241
kernel: kernel,
234242
initialFilesystem: initfs,
235-
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
243+
bootlog: imageStore.path.appendingPathComponent("bootlog.log").absolutePath(),
244+
rosetta: rosetta,
245+
nestedVirtualization: nestedVirtualization
236246
)
237247
}
238248

239249
/// Create a new manager with the provided kernel, initfs reference, image store
240-
/// and optional network implementation.
250+
/// and optional network implementation. This will use a Virtualization.framework
251+
/// backed VMM implicitly.
241252
public init(
242253
kernel: Kernel,
243254
initfsReference: String,
244255
imageStore: ImageStore,
245-
network: Network? = nil
256+
network: Network? = nil,
257+
rosetta: Bool = false,
258+
nestedVirtualization: Bool = false
246259
) async throws {
247260
self.imageStore = imageStore
248261
self.network = network
@@ -269,16 +282,21 @@ public struct ContainerManager: Sendable {
269282
self.vmm = VZVirtualMachineManager(
270283
kernel: kernel,
271284
initialFilesystem: initfs,
272-
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
285+
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath(),
286+
rosetta: rosetta,
287+
nestedVirtualization: nestedVirtualization
273288
)
274289
}
275290

276291
/// Create a new manager with the provided kernel and image reference for the initfs.
292+
/// This will use a Virtualization.framework backed VMM implicitly.
277293
public init(
278294
kernel: Kernel,
279295
initfsReference: String,
280296
root: URL? = nil,
281-
network: Network? = nil
297+
network: Network? = nil,
298+
rosetta: Bool = false,
299+
nestedVirtualization: Bool = false
282300
) async throws {
283301
if let root {
284302
self.imageStore = try ImageStore(path: root)
@@ -309,7 +327,9 @@ public struct ContainerManager: Sendable {
309327
self.vmm = VZVirtualMachineManager(
310328
kernel: kernel,
311329
initialFilesystem: initfs,
312-
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath()
330+
bootlog: self.imageStore.path.appendingPathComponent("bootlog.log").absolutePath(),
331+
rosetta: rosetta,
332+
nestedVirtualization: nestedVirtualization
313333
)
314334
}
315335

Sources/Containerization/LinuxContainer.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,14 @@ public final class LinuxContainer: Container, Sendable {
5252
public var interfaces: [any Interface] = []
5353
/// The Unix domain socket relays to setup for the container.
5454
public var sockets: [UnixSocketConfiguration] = []
55-
/// Whether rosetta x86-64 emulation should be setup for the container.
56-
public var rosetta: Bool = false
57-
/// Whether nested virtualization should be turned on for the container.
58-
public var virtualization: Bool = false
5955
/// The mounts for the container.
6056
public var mounts: [Mount] = LinuxContainer.defaultMounts()
6157
/// The DNS configuration for the container.
6258
public var dns: DNS?
6359
/// The hosts to add to /etc/hosts for the container.
6460
public var hosts: Hosts?
61+
/// Enable nested virtualization support.
62+
public var virtualization: Bool = false
6563

6664
public init() {}
6765
}
@@ -303,15 +301,27 @@ extension LinuxContainer {
303301
try await self.state.withLock { state in
304302
try state.validateForCreate()
305303

306-
let vm = try await self.vmm.create(container: self)
304+
let vmConfig = VMConfiguration(
305+
cpus: self.cpus,
306+
memoryInBytes: self.memoryInBytes,
307+
interfaces: self.interfaces,
308+
mountsByID: [self.id: [self.rootfs] + self.config.mounts],
309+
nestedVirtualization: self.config.virtualization
310+
)
311+
let creationConfig = StandardVMConfig(configuration: vmConfig)
312+
let vm = try await self.vmm.create(config: creationConfig)
313+
307314
try await vm.start()
308315
do {
309316
let relayManager = UnixSocketRelayManager(vm: vm)
310317
try await vm.withAgent { agent in
311318
try await agent.standardSetup()
312319

313320
// Mount the rootfs.
314-
var rootfs = vm.mounts[0].to
321+
guard let attachments = vm.mounts[self.id], let rootfsAttachment = attachments.first else {
322+
throw ContainerizationError(.notFound, message: "rootfs mount not found")
323+
}
324+
var rootfs = rootfsAttachment.to
315325
rootfs.destination = Self.guestRootfsPath(self.id)
316326
try await agent.mount(rootfs)
317327

@@ -364,7 +374,8 @@ extension LinuxContainer {
364374
do {
365375
var spec = self.generateRuntimeSpec()
366376
// We don't need the rootfs, nor do OCI runtimes want it included.
367-
spec.mounts = createdState.vm.mounts.dropFirst().map { $0.to }
377+
let containerMounts = createdState.vm.mounts[self.id] ?? []
378+
spec.mounts = containerMounts.dropFirst().map { $0.to }
368379

369380
let stdio = Self.setupIO(
370381
portAllocator: self.hostVsockPorts,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerizationOCI
18+
import Foundation
19+
20+
/// Protocol for VM creation configuration. Allows VMMs to extend with specific settings
21+
/// while maintaining a common core configuration.
22+
public protocol VMCreationConfig: Sendable {
23+
/// The common VM configuration that all VMMs must support.
24+
var configuration: VMConfiguration { get }
25+
}
26+
27+
/// Standard VM creation configuration with only common settings.
28+
public struct StandardVMConfig: VMCreationConfig {
29+
public var configuration: VMConfiguration
30+
31+
public init(configuration: VMConfiguration) {
32+
self.configuration = configuration
33+
}
34+
}
35+
36+
/// Configuration for creating a virtual machine instance.
37+
public struct VMConfiguration: Sendable {
38+
/// The amount of CPUs to allocate.
39+
public var cpus: Int
40+
/// The memory in bytes to allocate.
41+
public var memoryInBytes: UInt64
42+
/// The network interfaces to attach.
43+
public var interfaces: [any Interface]
44+
/// Mounts organized by metadata ID (e.g. container ID).
45+
/// Each ID maps to an array of mounts for that workload.
46+
public var mountsByID: [String: [Mount]]
47+
/// Optional file path to store serial boot logs.
48+
public var bootlog: URL?
49+
/// Enable nested virtualization support. If the VirtualMachineManager
50+
/// does not support this feature, it MUST return an .unsupported ContainerizationError.
51+
public var nestedVirtualization: Bool
52+
53+
public init(
54+
cpus: Int = 4,
55+
memoryInBytes: UInt64 = 1024 * 1024 * 1024,
56+
interfaces: [any Interface] = [],
57+
mountsByID: [String: [Mount]] = [:],
58+
bootlog: URL? = nil,
59+
nestedVirtualization: Bool = false
60+
) {
61+
self.cpus = cpus
62+
self.memoryInBytes = memoryInBytes
63+
self.interfaces = interfaces
64+
self.mountsByID = mountsByID
65+
self.bootlog = bootlog
66+
self.nestedVirtualization = nestedVirtualization
67+
}
68+
}

Sources/Containerization/VZVirtualMachineInstance.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import Virtualization
2828
struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
2929
typealias Agent = Vminitd
3030

31-
/// Attached mounts on the sandbox.
32-
public let mounts: [AttachedFilesystem]
31+
/// Attached mounts on the sandbox, organized by metadata ID.
32+
public let mounts: [String: [AttachedFilesystem]]
3333

3434
/// Returns the runtime state of the vm.
3535
public var state: VirtualMachineInstanceState {
@@ -47,8 +47,8 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
4747
public var rosetta: Bool
4848
/// Toggle nested virtualization support.
4949
public var nestedVirtualization: Bool
50-
/// Mount attachments.
51-
public var mounts: [Mount]
50+
/// Mount attachments organized by metadata ID.
51+
public var mountsByID: [String: [Mount]]
5252
/// Network interface attachments.
5353
public var interfaces: [any Interface]
5454
/// Kernel image.
@@ -63,7 +63,7 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
6363
self.memoryInBytes = 1024.mib()
6464
self.rosetta = false
6565
self.nestedVirtualization = false
66-
self.mounts = []
66+
self.mountsByID = [:]
6767
self.interfaces = []
6868
}
6969
}
@@ -317,8 +317,10 @@ extension VZVirtualMachineInstance.Configuration {
317317
config.bootLoader = loader
318318

319319
try initialFilesystem.configure(config: &config)
320-
for mount in self.mounts {
321-
try mount.configure(config: &config)
320+
for (_, mounts) in self.mountsByID {
321+
for mount in mounts {
322+
try mount.configure(config: &config)
323+
}
322324
}
323325

324326
let platform = VZGenericPlatformConfiguration()
@@ -337,7 +339,7 @@ extension VZVirtualMachineInstance.Configuration {
337339
return config
338340
}
339341

340-
func mountAttachments() throws -> [AttachedFilesystem] {
342+
func mountAttachments() throws -> [String: [AttachedFilesystem]] {
341343
let allocator = Character.blockDeviceTagAllocator()
342344
if let initialFilesystem {
343345
// When the initial filesystem is a blk, allocate the first letter "vd(a)"
@@ -347,11 +349,15 @@ extension VZVirtualMachineInstance.Configuration {
347349
}
348350
}
349351

350-
var attachments: [AttachedFilesystem] = []
351-
for mount in self.mounts {
352-
attachments.append(try .init(mount: mount, allocator: allocator))
352+
var attachmentsByID: [String: [AttachedFilesystem]] = [:]
353+
for (id, mounts) in self.mountsByID {
354+
var attachments: [AttachedFilesystem] = []
355+
for mount in mounts {
356+
attachments.append(try .init(mount: mount, allocator: allocator))
357+
}
358+
attachmentsByID[id] = attachments
353359
}
354-
return attachments
360+
return attachmentsByID
355361
}
356362
}
357363

Sources/Containerization/VZVirtualMachineManager.swift

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,47 +23,53 @@ import Logging
2323
/// A virtualization.framework backed `VirtualMachineManager` implementation.
2424
public struct VZVirtualMachineManager: VirtualMachineManager {
2525
private let kernel: Kernel
26-
private let bootlog: String?
2726
private let initialFilesystem: Mount
27+
private let bootlog: String?
28+
private let rosetta: Bool
29+
private let nestedVirtualization: Bool
2830
private let logger: Logger?
2931

3032
public init(
3133
kernel: Kernel,
3234
initialFilesystem: Mount,
3335
bootlog: String? = nil,
36+
rosetta: Bool = false,
37+
nestedVirtualization: Bool = false,
3438
logger: Logger? = nil
3539
) {
3640
self.kernel = kernel
37-
self.bootlog = bootlog
3841
self.initialFilesystem = initialFilesystem
42+
self.bootlog = bootlog
43+
self.rosetta = rosetta
44+
self.nestedVirtualization = nestedVirtualization
3945
self.logger = logger
4046
}
4147

42-
public func create(container: Container) throws -> any VirtualMachineInstance {
43-
guard let c = container as? LinuxContainer else {
44-
throw ContainerizationError(
45-
.invalidArgument,
46-
message: "provided container is not a LinuxContainer"
47-
)
48-
}
48+
public func create(config: some VMCreationConfig) throws -> any VirtualMachineInstance {
49+
let vmConfig = config.configuration
50+
51+
// Use nested virtualization if requested in config or set as default in manager
52+
let useNestedVirtualization = vmConfig.nestedVirtualization || self.nestedVirtualization
4953

5054
return try VZVirtualMachineInstance(
5155
logger: self.logger,
52-
with: { config in
53-
config.cpus = container.cpus
54-
config.memoryInBytes = container.memoryInBytes
56+
with: { instanceConfig in
57+
instanceConfig.cpus = vmConfig.cpus
58+
instanceConfig.memoryInBytes = vmConfig.memoryInBytes
5559

56-
config.kernel = self.kernel
57-
config.initialFilesystem = self.initialFilesystem
60+
instanceConfig.kernel = self.kernel
61+
instanceConfig.initialFilesystem = self.initialFilesystem
5862

59-
config.interfaces = container.interfaces
60-
if let bootlog {
61-
config.bootlog = URL(filePath: bootlog)
63+
instanceConfig.interfaces = vmConfig.interfaces
64+
if let bootlog = vmConfig.bootlog {
65+
instanceConfig.bootlog = bootlog
66+
} else if let bootlog = self.bootlog {
67+
instanceConfig.bootlog = URL(filePath: bootlog)
6268
}
63-
config.rosetta = c.config.rosetta
64-
config.nestedVirtualization = c.config.virtualization
69+
instanceConfig.rosetta = self.rosetta
70+
instanceConfig.nestedVirtualization = useNestedVirtualization
6571

66-
config.mounts = [c.rootfs] + c.config.mounts
72+
instanceConfig.mountsByID = vmConfig.mountsByID
6773
})
6874
}
6975
}

Sources/Containerization/VirtualMachineInstance.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public protocol VirtualMachineInstance: Sendable {
3333
// The state of the virtual machine.
3434
var state: VirtualMachineInstanceState { get }
3535

36-
var mounts: [AttachedFilesystem] { get }
36+
var mounts: [String: [AttachedFilesystem]] { get }
3737
/// Dial the Agent. It's up the VirtualMachineInstance to determine
3838
/// what port the agent is listening on.
3939
func dialAgent() async throws -> Agent

Sources/Containerization/VirtualMachineManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616

1717
/// A protocol to implement for virtual machine isolated containers.
1818
public protocol VirtualMachineManager: Sendable {
19-
func create(container: Container) async throws -> any VirtualMachineInstance
19+
func create(config: some VMCreationConfig) async throws -> any VirtualMachineInstance
2020
}

0 commit comments

Comments
 (0)