diff --git a/Sources/Containerization/FileMount.swift b/Sources/Containerization/FileMount.swift index c61aadeb..8965f3a5 100644 --- a/Sources/Containerization/FileMount.swift +++ b/Sources/Containerization/FileMount.swift @@ -118,7 +118,7 @@ extension FileMountContext { let directoryShare = Mount.share( source: prepared.tempDirectory.path, destination: "/.file-mount-holding", - options: mount.options.filter { $0 != "bind" }, + options: [], runtimeOptions: runtimeOpts ) transformed.append(directoryShare) @@ -197,12 +197,14 @@ extension FileMountContext { let guestPath = "/run/file-mounts/\(prepared.tag)" try await agent.mkdir(path: guestPath, all: true, perms: 0o755) + + let virtiofsMountOptions = prepared.options.filter { $0 != "bind" } try await agent.mount( ContainerizationOCI.Mount( type: "virtiofs", source: attached.source, destination: guestPath, - options: [] + options: virtiofsMountOptions )) preparedMounts[i].guestHoldingPath = guestPath @@ -212,10 +214,27 @@ extension FileMountContext { extension FileMountContext { /// Get the bind mounts to append to the OCI spec. - func ociBindMounts() -> [ContainerizationOCI.Mount] { - preparedMounts.compactMap { prepared in + /// - Throws: If temp files have been cleaned up before the container starts + func ociBindMounts() throws -> [ContainerizationOCI.Mount] { + try preparedMounts.map { prepared in guard let guestPath = prepared.guestHoldingPath else { - return nil + // This should never happen if mountHoldingDirectories was called. + throw ContainerizationError( + .internalError, + message: "guestHoldingPath not set for file mount \(prepared.hostFilePath)" + ) + } + + // Verify the temp directory and file still exist on the host. + // If they've been cleaned up (e.g., by system temp cleanup), the virtiofs + // share will be empty and the bind mount will fail. + let fileInTempDir = prepared.tempDirectory.appendingPathComponent(prepared.filename) + guard FileManager.default.fileExists(atPath: fileInTempDir.path) else { + throw ContainerizationError( + .notFound, + message: "file mount temp file was cleaned up before container start: \(fileInTempDir.path), " + + "original host file: \(prepared.hostFilePath)" + ) } return ContainerizationOCI.Mount( diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index c0622049..a960a767 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -526,7 +526,7 @@ extension LinuxContainer { containerMounts.dropFirst() .filter { !holdingTags.contains($0.source) } .map { $0.to } - + createdState.fileMountContext.ociBindMounts() + + (try createdState.fileMountContext.ociBindMounts()) let stdio = IOUtil.setup( portAllocator: self.hostVsockPorts, diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 39165ca4..d2db3d63 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -498,7 +498,7 @@ extension LinuxPod { containerMounts.dropFirst() .filter { !holdingTags.contains($0.source) } .map { $0.to } - + container.fileMountContext.ociBindMounts() + + (try container.fileMountContext.ociBindMounts()) // Configure namespaces for the container var namespaces: [LinuxNamespace] = [ diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 893fbe9b..0090eeee 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -2101,11 +2101,13 @@ extension IntegrationSuite { try testContent.write(to: hostFile, atomically: true, encoding: .utf8) let buffer = BufferWriter() + let stderrBuffer = BufferWriter() let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in config.process.arguments = ["cat", "/etc/myconfig.txt"] // Mount a single file using virtiofs share config.mounts.append(.share(source: hostFile.path, destination: "/etc/myconfig.txt")) config.process.stdout = buffer + config.process.stderr = stderrBuffer config.bootLog = bs.bootLog } @@ -2117,7 +2119,11 @@ extension IntegrationSuite { try await container.stop() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "process status \(status) != 0") + let stderrOutput = String(data: stderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + let stdoutOutput = String(data: buffer.data, encoding: .utf8) ?? "(no stdout)" + throw IntegrationError.assert( + msg: "process status \(status) != 0, stdout: '\(stdoutOutput)', stderr: '\(stderrOutput)'" + ) } guard let output = String(data: buffer.data, encoding: .utf8) else { @@ -2158,16 +2164,19 @@ extension IntegrationSuite { // First verify we can read the file let readBuffer = BufferWriter() + let readStderrBuffer = BufferWriter() let readExec = try await container.exec("read-file") { config in config.arguments = ["cat", "/etc/readonly.txt"] config.stdout = readBuffer + config.stderr = readStderrBuffer } try await readExec.start() var status = try await readExec.wait() try await readExec.delete() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "read status \(status) != 0") + let stderrOutput = String(data: readStderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + throw IntegrationError.assert(msg: "read status \(status) != 0, stderr: '\(stderrOutput)'") } guard String(data: readBuffer.data, encoding: .utf8) == testContent else { @@ -2220,15 +2229,18 @@ extension IntegrationSuite { // Write new content from inside the container let newContent = "modified from container" + let writeStderrBuffer = BufferWriter() let writeExec = try await container.exec("write-file") { config in config.arguments = ["sh", "-c", "echo -n '\(newContent)' > /etc/writeable.txt"] + config.stderr = writeStderrBuffer } try await writeExec.start() let status = try await writeExec.wait() try await writeExec.delete() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "write status \(status) != 0") + let stderrOutput = String(data: writeStderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + throw IntegrationError.assert(msg: "write status \(status) != 0, stderr: '\(stderrOutput)'") } try await container.kill(SIGKILL) @@ -2273,16 +2285,19 @@ extension IntegrationSuite { // Read the file to verify content let readBuffer = BufferWriter() + let readStderrBuffer = BufferWriter() let readExec = try await container.exec("read-file") { config in config.arguments = ["cat", "/etc/config.txt"] config.stdout = readBuffer + config.stderr = readStderrBuffer } try await readExec.start() var status = try await readExec.wait() try await readExec.delete() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "read status \(status) != 0") + let stderrOutput = String(data: readStderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + throw IntegrationError.assert(msg: "read status \(status) != 0, stderr: '\(stderrOutput)'") } guard String(data: readBuffer.data, encoding: .utf8) == initialContent else { @@ -2291,15 +2306,18 @@ extension IntegrationSuite { // Write new content from container let newContent = "modified via symlink mount" + let writeStderrBuffer = BufferWriter() let writeExec = try await container.exec("write-file") { config in config.arguments = ["sh", "-c", "echo -n '\(newContent)' > /etc/config.txt"] + config.stderr = writeStderrBuffer } try await writeExec.start() status = try await writeExec.wait() try await writeExec.delete() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "write status \(status) != 0") + let stderrOutput = String(data: writeStderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + throw IntegrationError.assert(msg: "write status \(status) != 0, stderr: '\(stderrOutput)'") } try await container.kill(SIGKILL) diff --git a/Sources/Integration/PodTests.swift b/Sources/Integration/PodTests.swift index ab67ed21..6f53e3d9 100644 --- a/Sources/Integration/PodTests.swift +++ b/Sources/Integration/PodTests.swift @@ -891,11 +891,13 @@ extension IntegrationSuite { } let buffer = BufferWriter() + let stderrBuffer = BufferWriter() try await pod.addContainer("container1", rootfs: bs.rootfs) { config in config.process.arguments = ["cat", "/etc/myconfig.txt"] // Mount a single file using virtiofs share config.mounts.append(.share(source: hostFile.path, destination: "/etc/myconfig.txt")) config.process.stdout = buffer + config.process.stderr = stderrBuffer } do { @@ -906,7 +908,11 @@ extension IntegrationSuite { try await pod.stop() guard status.exitCode == 0 else { - throw IntegrationError.assert(msg: "process status \(status) != 0") + let stderrOutput = String(data: stderrBuffer.data, encoding: .utf8) ?? "(no stderr)" + let stdoutOutput = String(data: buffer.data, encoding: .utf8) ?? "(no stdout)" + throw IntegrationError.assert( + msg: "process status \(status) != 0, stdout: '\(stdoutOutput)', stderr: '\(stderrOutput)'" + ) } guard let output = String(data: buffer.data, encoding: .utf8) else {