From 1b00e1a188b33fc1beb5c6d3d55b6e9cd62f0b17 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Thu, 31 Jul 2025 23:15:21 -0700 Subject: [PATCH] Allow reusing virtiofs mounts Fwiw, this is a bit odd, but it's also very simple to support and feels odd that this was failing today. This also adds in a tiny bit of logic in hashMountSource to resolve the path of symlinks, so now even symlinks to the same dir can be supplied N times without a new virtiofs device. --- Sources/Containerization/Hash.swift | 9 ++++- Sources/Containerization/Mount.swift | 9 ++++- .../VZVirtualMachineInstance.swift | 12 ++++-- Sources/Integration/Suite.swift | 1 + Sources/Integration/VMTests.swift | 37 ++++++++++++++++++- 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Sources/Containerization/Hash.swift b/Sources/Containerization/Hash.swift index 623ff027..d470683a 100644 --- a/Sources/Containerization/Hash.swift +++ b/Sources/Containerization/Hash.swift @@ -18,10 +18,15 @@ import Crypto import ContainerizationError +import Foundation func hashMountSource(source: String) throws -> String { - guard let data = source.data(using: .utf8) else { - throw ContainerizationError(.invalidArgument, message: "\(source) could not be converted to Data") + let resolvedSource = URL(fileURLWithPath: source) + .resolvingSymlinksInPath() + .path + + guard let data = resolvedSource.data(using: .utf8) else { + throw ContainerizationError(.invalidArgument, message: "\(resolvedSource) could not be converted to Data") } return String(SHA256.hash(data: data).encoded.prefix(36)) } diff --git a/Sources/Containerization/Mount.swift b/Sources/Containerization/Mount.swift index b16cb263..e3dd7571 100644 --- a/Sources/Containerization/Mount.swift +++ b/Sources/Containerization/Mount.swift @@ -132,7 +132,7 @@ public struct Mount: Sendable { #if os(macOS) extension Mount { - func configure(config: inout VZVirtualMachineConfiguration) throws { + func configure(config: inout VZVirtualMachineConfiguration, usedVirtioFSTags: inout Set) throws { switch self.runtimeOptions { case .virtioblk(let options): let device = try VZDiskImageStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options) @@ -144,6 +144,13 @@ extension Mount { } let name = try hashMountSource(source: self.source) + + // Skip creating VZVirtioFileSystemDeviceConfiguration if tag already exists. + if usedVirtioFSTags.contains(name) { + return + } + + usedVirtioFSTags.insert(name) let urlSource = URL(fileURLWithPath: source) let device = VZVirtioFileSystemDeviceConfiguration(tag: name) diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index 2042c1d1..1dc56973 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -57,6 +57,8 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable { public var initialFilesystem: Mount? /// File path to store the sandbox boot logs. public var bootlog: URL? + /// Set of virtiofs tags that have already been configured to avoid duplicates. + var usedVirtioFSTags: Set = [] init() { self.cpus = 4 @@ -65,6 +67,7 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable { self.nestedVirtualization = false self.mounts = [] self.interfaces = [] + self.usedVirtioFSTags = [] } } @@ -86,6 +89,7 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable { } init(group: MultiThreadedEventLoopGroup, config: Configuration, logger: Logger?) throws { + var mutableConfig = config self.config = config self.group = group self.lock = .init() @@ -95,7 +99,7 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable { self.timeSyncer = .init(logger: logger) self.vm = VZVirtualMachine( - configuration: try config.toVZ(), + configuration: try mutableConfig.toVZ(), queue: self.queue ) } @@ -243,7 +247,7 @@ extension VZVirtualMachineInstance.Configuration { return [c] } - func toVZ() throws -> VZVirtualMachineConfiguration { + mutating func toVZ() throws -> VZVirtualMachineConfiguration { var config = VZVirtualMachineConfiguration() config.cpuCount = self.cpus @@ -301,9 +305,9 @@ extension VZVirtualMachineInstance.Configuration { loader.commandLine = kernel.linuxCommandline(initialFilesystem: initialFilesystem) config.bootLoader = loader - try initialFilesystem.configure(config: &config) + try initialFilesystem.configure(config: &config, usedVirtioFSTags: &usedVirtioFSTags) for mount in self.mounts { - try mount.configure(config: &config) + try mount.configure(config: &config, usedVirtioFSTags: &usedVirtioFSTags) } let platform = VZGenericPlatformConfiguration() diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index 94896b9c..03e34f73 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -209,6 +209,7 @@ struct IntegrationSuite: AsyncParsableCommand { "container hostname": testHostname, "container hosts": testHostsFile, "container mount": testMounts, + "container mount duplicate": testDuplicateMount, "nested virt": testNestedVirtualizationEnabled, "container manager": testContainerManagerCreate, ] diff --git a/Sources/Integration/VMTests.swift b/Sources/Integration/VMTests.swift index 7ad9bb53..c9584c0f 100644 --- a/Sources/Integration/VMTests.swift +++ b/Sources/Integration/VMTests.swift @@ -27,8 +27,10 @@ extension IntegrationSuite { let bs = try await bootstrap() let buffer = BufferWriter() + let directory = try createMountDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + 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 @@ -121,6 +123,39 @@ extension IntegrationSuite { } } + func testDuplicateMount() async throws { + let id = "test-duplicate-mounts" + + let bs = try await bootstrap() + let buffer = BufferWriter() + let directory = try createMountDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["/bin/sh", "-c", "cat /mnt1/hi.txt && cat /mnt2/hi.txt"] + // Mount the same directory to two different destinations + config.mounts.append(.share(source: directory.path, destination: "/mnt1")) + config.mounts.append(.share(source: directory.path, destination: "/mnt2")) + config.process.stdout = buffer + } + + try await container.create() + try await container.start() + + let status = try await container.wait() + try await container.stop() + + guard status == 0 else { + throw IntegrationError.assert(msg: "process status \(status) != 0") + } + + let value = String(data: buffer.data, encoding: .utf8) + guard value == "hellohello" else { + throw IntegrationError.assert( + msg: "process should have returned 'hellohello' != '\(String(data: buffer.data, encoding: .utf8)!)'") + } + } + private func createMountDirectory() throws -> URL { let dir = FileManager.default.uniqueTemporaryDirectory(create: true) try "hello".write(to: dir.appendingPathComponent("hi.txt"), atomically: true, encoding: .utf8)