LinuxContainer/LinuxPod: Single file support (#487)

Fixes #79

This adds support for single file (virtiofs based) mounts to the two
main container types. This is transparent, and doesn't need to be setup
by a user, so there is no visible API change. Virtiofs does not support
single file mounts today, and it would be less than ideal from a
security standpoint to just expose the parent directory of the file to
the vm and then bind mount in the file, so we chose the following:

1. Create a temp directory the container types will manage.
2. Hardlink in the file we want.
3. Share that tempdir into the vm.
4. Finally bind mount in the file into the container.

The main goal I wanted for this is to leave the logic out of the
`VirtualMachineInstance`. I really want this to just be a little dance
the container types do, and leave the vm out of it. Because of that,
most of the logic is shoved in a new `FileMountContext` type that does
the dirty tricks of rewriting what the user asked for to be a bind mount
from a temporary holding spot we'll use in the guest for these temp
directories.

One of the downsides to this is today the tempdirs are solely on the
main volume, so cross volume will need to copy the file to the tempdir,
so writes won't get written back.

Co-authored-by: Dean Coulstock <d_coulstock@apple.com>
Co-authored-by: Jaewon Hur <hurjaewon@snu.ac.kr>
This commit is contained in:
Danny Canter
2026-01-22 20:57:58 -08:00
committed by GitHub
co-authored by Dean Coulstock Jaewon Hur
parent 1d9140cc8a
commit 1a017417bc
6 changed files with 598 additions and 6 deletions
+241
View File
@@ -0,0 +1,241 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
#if os(macOS)
import ContainerizationError
import ContainerizationOCI
import Foundation
/// Manages single-file mounts by transforming them into virtiofs directory shares
/// plus bind mounts.
///
/// Since virtiofs only supports sharing directories, mounting a single file without
/// exposing the other potential files in that directory needs a little bit of a "hack".
/// The one we've landed on is:
///
/// 1. Creating a temporary directory containing a hardlink to the file
/// 2. Sharing that directory via virtiofs to a holding location in the guest
/// 3. Bind mounting the specific file from the holding location to the final destination
///
/// This type handles all three steps transparently.
struct FileMountContext: Sendable {
/// Metadata for a single prepared file mount.
struct PreparedMount: Sendable {
/// Original file path on host
let hostFilePath: String
/// Where the user wants the file in the container
let containerDestination: String
/// Just the filename
let filename: String
/// Temp directory containing the hardlinked file
let tempDirectory: URL
/// The virtiofs tag (hash of temp dir path). Used to find the AttachedFilesystem
let tag: String
/// Mount options from the original mount
let options: [String]
/// Where we mounted the share in the guest (set after mountHoldingDirectories)
var guestHoldingPath: String?
}
/// Prepared file mounts for this context
var preparedMounts: [PreparedMount]
/// The transformed mounts to pass to the VM (files replaced with directory shares)
private(set) var transformedMounts: [Mount]
private init() {
self.preparedMounts = []
self.transformedMounts = []
}
/// Returns true if there are any file mounts that need handling.
var hasFileMounts: Bool {
!preparedMounts.isEmpty
}
/// Returns the set of virtiofs tags for file mount holding directories.
/// These should be filtered out from OCI spec mounts since we mount them
/// separately under /run.
var holdingDirectoryTags: Set<String> {
Set(preparedMounts.map { $0.tag })
}
}
extension FileMountContext {
/// Prepare mounts for a container, detecting file mounts and transforming them.
///
/// This method stats each virtiofs mount source. If it's a regular file rather than
/// a directory, it creates a temporary directory with a hardlink to the file and
/// substitutes a directory share for the original mount.
///
/// - Parameter mounts: The original mounts from the container config
/// - Returns: A FileMountContext containing transformed mounts and tracking info
static func prepare(mounts: [Mount]) throws -> FileMountContext {
var context = FileMountContext()
var transformed: [Mount] = []
for mount in mounts {
// Only virtiofs mounts can be files
guard case .virtiofs(let runtimeOpts) = mount.runtimeOptions else {
transformed.append(mount)
continue
}
// Stat the source to see if it's a file
let fm = FileManager.default
var isDirectory: ObjCBool = false
guard fm.fileExists(atPath: mount.source, isDirectory: &isDirectory) else {
// Doesn't exist. Let the normal flow handle the error
transformed.append(mount)
continue
}
if isDirectory.boolValue {
// It's a directory, pass through unchanged
transformed.append(mount)
continue
}
// It's a file, so prepare it.
let prepared = try context.prepareFileMount(mount: mount, runtimeOptions: runtimeOpts)
// Create a regular directory share for the temp directory.
// The destination here is unused. We'll mount it ourselves to a location under /run.
let directoryShare = Mount.share(
source: prepared.tempDirectory.path,
destination: "/.file-mount-holding",
options: mount.options.filter { $0 != "bind" },
runtimeOptions: runtimeOpts
)
transformed.append(directoryShare)
}
context.transformedMounts = transformed
return context
}
private mutating func prepareFileMount(
mount: Mount,
runtimeOptions: [String]
) throws -> PreparedMount {
let resolvedSource = URL(fileURLWithPath: mount.source).resolvingSymlinksInPath()
let sourceURL = URL(fileURLWithPath: mount.source)
let filename = sourceURL.lastPathComponent
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("containerization-file-mounts")
.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(
at: tempDir,
withIntermediateDirectories: true
)
// Hardlink the file (falls back to copy if cross-filesystem)
let destURL = tempDir.appendingPathComponent(filename)
do {
try FileManager.default.linkItem(at: resolvedSource, to: destURL)
} catch {
// Hardlink failed. Fall back to copy
try FileManager.default.copyItem(at: resolvedSource, to: destURL)
}
let tag = try hashMountSource(source: tempDir.path)
let prepared = PreparedMount(
hostFilePath: mount.source,
containerDestination: mount.destination,
filename: filename,
tempDirectory: tempDir,
tag: tag,
options: mount.options,
guestHoldingPath: nil
)
preparedMounts.append(prepared)
return prepared
}
}
extension FileMountContext {
/// Mount the holding directories in the guest for all file mounts.
/// - Parameters:
/// - vmMounts: The AttachedFilesystem array from the VM for this container
/// - agent: The VM agent for RPCs
mutating func mountHoldingDirectories(
vmMounts: [AttachedFilesystem],
agent: any VirtualMachineAgent
) async throws {
for i in preparedMounts.indices {
let prepared = preparedMounts[i]
// Find the attached filesystem by matching the virtiofs tag
guard
let attached = vmMounts.first(where: {
$0.type == "virtiofs" && $0.source == prepared.tag
})
else {
throw ContainerizationError(
.notFound,
message: "could not find attached filesystem for file mount \(prepared.hostFilePath)"
)
}
let guestPath = "/run/file-mounts/\(prepared.tag)"
try await agent.mkdir(path: guestPath, all: true, perms: 0o755)
try await agent.mount(
ContainerizationOCI.Mount(
type: "virtiofs",
source: attached.source,
destination: guestPath,
options: []
))
preparedMounts[i].guestHoldingPath = guestPath
}
}
}
extension FileMountContext {
/// Get the bind mounts to append to the OCI spec.
func ociBindMounts() -> [ContainerizationOCI.Mount] {
preparedMounts.compactMap { prepared in
guard let guestPath = prepared.guestHoldingPath else {
return nil
}
return ContainerizationOCI.Mount(
type: "none",
source: "\(guestPath)/\(prepared.filename)",
destination: prepared.containerDestination,
options: ["bind"] + prepared.options
)
}
}
}
extension FileMountContext {
/// Clean up temp directories.
func cleanup() {
let fm = FileManager.default
for prepared in preparedMounts {
try? fm.removeItem(at: prepared.tempDirectory)
}
}
}
#endif
+34 -3
View File
@@ -126,6 +126,7 @@ public final class LinuxContainer: Container, Sendable {
struct CreatedState: Sendable {
let vm: any VirtualMachineInstance
let relayManager: UnixSocketRelayManager
var fileMountContext: FileMountContext
}
struct StartedState: Sendable {
@@ -133,12 +134,14 @@ public final class LinuxContainer: Container, Sendable {
let process: LinuxProcess
let relayManager: UnixSocketRelayManager
var vendedProcesses: [String: LinuxProcess]
let fileMountContext: FileMountContext
init(_ state: CreatedState, process: LinuxProcess) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = process
self.vendedProcesses = [:]
self.fileMountContext = state.fileMountContext
}
init(_ state: PausedState) {
@@ -146,6 +149,7 @@ public final class LinuxContainer: Container, Sendable {
self.relayManager = state.relayManager
self.process = state.process
self.vendedProcesses = state.vendedProcesses
self.fileMountContext = state.fileMountContext
}
}
@@ -154,12 +158,14 @@ public final class LinuxContainer: Container, Sendable {
let relayManager: UnixSocketRelayManager
let process: LinuxProcess
var vendedProcesses: [String: LinuxProcess]
let fileMountContext: FileMountContext
init(_ state: StartedState) {
self.vm = state.vm
self.relayManager = state.relayManager
self.process = state.process
self.vendedProcesses = state.vendedProcesses
self.fileMountContext = state.fileMountContext
}
}
@@ -417,11 +423,16 @@ extension LinuxContainer {
ProcessInfo.processInfo.physicalMemory
)
// Prepare file mounts. This transforms single-file mounts into directory shares.
let fileMountContext = try FileMountContext.prepare(mounts: self.config.mounts)
// This is dumb, but alas.
let fileMountContextHolder = Mutex<FileMountContext>(fileMountContext)
let vmConfig = VMConfiguration(
cpus: self.cpus,
memoryInBytes: vmMemory,
interfaces: self.interfaces,
mountsByID: [self.id: [modifiedRootfs] + self.config.mounts],
mountsByID: [self.id: [modifiedRootfs] + fileMountContext.transformedMounts],
bootLog: self.config.bootLog,
nestedVirtualization: self.config.virtualization
)
@@ -442,6 +453,17 @@ extension LinuxContainer {
rootfs.destination = Self.guestRootfsPath(self.id)
try await agent.mount(rootfs)
// Mount file mount holding directories under /run.
if fileMountContext.hasFileMounts {
let containerMounts = vm.mounts[self.id] ?? []
var ctx = fileMountContextHolder.withLock { $0 }
try await ctx.mountHoldingDirectories(
vmMounts: containerMounts,
agent: agent
)
fileMountContextHolder.withLock { $0 = ctx }
}
// Start up our friendly unix socket relays.
for socket in self.config.sockets {
try await self.relayUnixSocket(
@@ -478,7 +500,7 @@ extension LinuxContainer {
}
}
state = .created(.init(vm: vm, relayManager: relayManager))
state = .created(.init(vm: vm, relayManager: relayManager, fileMountContext: fileMountContextHolder.withLock { $0 }))
} catch {
try? await relayManager.stopAll()
try? await vm.stop()
@@ -497,8 +519,14 @@ extension LinuxContainer {
do {
var spec = self.generateRuntimeSpec()
// We don't need the rootfs, nor do OCI runtimes want it included.
// Also filter out file mount holding directories. We'll mount those separately under /run.
let containerMounts = createdState.vm.mounts[self.id] ?? []
spec.mounts = containerMounts.dropFirst().map { $0.to }
let holdingTags = createdState.fileMountContext.holdingDirectoryTags
spec.mounts =
containerMounts.dropFirst()
.filter { !holdingTags.contains($0.source) }
.map { $0.to }
+ createdState.fileMountContext.ociBindMounts()
let stdio = IOUtil.setup(
portAllocator: self.hostVsockPorts,
@@ -646,6 +674,9 @@ extension LinuxContainer {
firstError = firstError ?? error
}
// Clean up file mount temporary directories.
startedState.fileMountContext.cleanup()
do {
try await vm.stop()
state = .stopped
+39 -3
View File
@@ -83,6 +83,7 @@ public final class LinuxPod: Sendable {
let config: ContainerConfiguration
var state: ContainerState
var process: LinuxProcess?
var fileMountContext: FileMountContext
enum ContainerState: Sendable {
case registered
@@ -273,12 +274,16 @@ extension LinuxPod {
var config = ContainerConfiguration()
try configuration(&config)
// Prepare file mounts - transforms single-file mounts into directory shares.
let fileMountContext = try FileMountContext.prepare(mounts: config.mounts)
state.containers[id] = PodContainer(
id: id,
rootfs: rootfs,
config: config,
state: .registered,
process: nil
process: nil,
fileMountContext: fileMountContext
)
}
}
@@ -293,11 +298,12 @@ extension LinuxPod {
// Build mountsByID for all containers.
// Strip "ro" from rootfs options - we handle readonly via the OCI spec's
// root.readonly field and remount in vmexec after setup is complete.
// Use transformedMounts from fileMountContext (file mounts become directory shares).
var mountsByID: [String: [Mount]] = [:]
for (id, container) in state.containers {
var modifiedRootfs = container.rootfs
modifiedRootfs.options.removeAll(where: { $0 == "ro" })
mountsByID[id] = [modifiedRootfs] + container.config.mounts
mountsByID[id] = [modifiedRootfs] + container.fileMountContext.transformedMounts
}
let vmConfig = VMConfiguration(
@@ -317,6 +323,7 @@ extension LinuxPod {
let containers = state.containers
let shareProcessNamespace = self.config.shareProcessNamespace
let pauseProcessHolder = Mutex<LinuxProcess?>(nil)
let fileMountContextUpdates = Mutex<[String: FileMountContext]>([:])
try await vm.withAgent { agent in
try await agent.standardSetup()
@@ -383,6 +390,19 @@ extension LinuxPod {
try await agent.mount(rootfs)
}
// Mount file mount holding directories under /run for each container.
for (id, container) in containers {
if container.fileMountContext.hasFileMounts {
var ctx = container.fileMountContext
let containerMounts = vm.mounts[id] ?? []
try await ctx.mountHoldingDirectories(
vmMounts: containerMounts,
agent: agent
)
fileMountContextUpdates.withLock { $0[id] = ctx }
}
}
// Start up unix socket relays for each container
for (_, container) in containers {
for socket in container.config.sockets {
@@ -427,6 +447,12 @@ extension LinuxPod {
state.pauseProcess = pauseProcessHolder.withLock { $0 }
// Apply file mount context updates.
let updates = fileMountContextUpdates.withLock { $0 }
for (id, ctx) in updates {
state.containers[id]?.fileMountContext = ctx
}
// Transition all containers to created state
for id in state.containers.keys {
state.containers[id]?.state = .created
@@ -465,8 +491,14 @@ extension LinuxPod {
do {
var spec = self.generateRuntimeSpec(containerID: containerID, config: container.config, rootfs: container.rootfs)
// We don't need the rootfs, nor do OCI runtimes want it included.
// Also filter out file mount holding directories - we mount those separately under /run.
let containerMounts = createdState.vm.mounts[containerID] ?? []
spec.mounts = containerMounts.dropFirst().map { $0.to }
let holdingTags = container.fileMountContext.holdingDirectoryTags
spec.mounts =
containerMounts.dropFirst()
.filter { !holdingTags.contains($0.source) }
.map { $0.to }
+ container.fileMountContext.ociBindMounts()
// Configure namespaces for the container
var namespaces: [LinuxNamespace] = [
@@ -620,6 +652,10 @@ extension LinuxPod {
try? await process.delete()
container.process = nil
container.state = .stopped
// Clean up file mount temporary directories.
container.fileMountContext.cleanup()
state.containers[containerID] = container
}
}
+229
View File
@@ -2088,4 +2088,233 @@ extension IntegrationSuite {
throw error
}
}
func testSingleFileMount() async throws {
let id = "test-single-file-mount"
let bs = try await bootstrap(id)
// Create a temp file with known content
let testContent = "Hello from single file mount!"
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("config.txt")
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
let buffer = 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.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
let status = try await container.wait()
try await container.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
}
guard output == testContent else {
throw IntegrationError.assert(
msg: "expected '\(testContent)', got '\(output)'")
}
} catch {
try? await container.stop()
throw error
}
}
func testSingleFileMountReadOnly() async throws {
let id = "test-single-file-mount-readonly"
let bs = try await bootstrap(id)
// Create a temp file with known content
let testContent = "Read-only file content"
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("readonly.txt")
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
// Mount a single file as read-only
config.mounts.append(.share(source: hostFile.path, destination: "/etc/readonly.txt", options: ["ro"]))
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// First verify we can read the file
let readBuffer = BufferWriter()
let readExec = try await container.exec("read-file") { config in
config.arguments = ["cat", "/etc/readonly.txt"]
config.stdout = readBuffer
}
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")
}
guard String(data: readBuffer.data, encoding: .utf8) == testContent else {
throw IntegrationError.assert(msg: "file content mismatch")
}
// Now try to write to the file - should fail
let writeExec = try await container.exec("write-file") { config in
config.arguments = ["sh", "-c", "echo 'modified' > /etc/readonly.txt"]
}
try await writeExec.start()
status = try await writeExec.wait()
try await writeExec.delete()
// Write should fail on a read-only mount
guard status.exitCode != 0 else {
throw IntegrationError.assert(msg: "write should have failed on read-only mount")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
func testSingleFileMountWriteBack() async throws {
let id = "test-single-file-mount-write-back"
let bs = try await bootstrap(id)
// Create a temp file with initial content
let initialContent = "initial content"
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("writeable.txt")
try initialContent.write(to: hostFile, atomically: true, encoding: .utf8)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
// Mount a single file (writable by default)
config.mounts.append(.share(source: hostFile.path, destination: "/etc/writeable.txt"))
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// Write new content from inside the container
let newContent = "modified from container"
let writeExec = try await container.exec("write-file") { config in
config.arguments = ["sh", "-c", "echo -n '\(newContent)' > /etc/writeable.txt"]
}
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")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
let hostContent = try String(contentsOf: hostFile, encoding: .utf8)
guard hostContent == newContent else {
throw IntegrationError.assert(
msg: "expected '\(newContent)' on host, got '\(hostContent)'")
}
} catch {
try? await container.stop()
throw error
}
}
func testSingleFileMountSymlink() async throws {
let id = "test-single-file-mount-symlink"
let bs = try await bootstrap(id)
// Create a temp directory with a real file and a symlink to it
let tempDir = FileManager.default.uniqueTemporaryDirectory(create: true)
let realFile = tempDir.appendingPathComponent("realfile.txt")
let symlinkFile = tempDir.appendingPathComponent("symlink.txt")
let initialContent = "content via symlink"
try initialContent.write(to: realFile, atomically: true, encoding: .utf8)
try FileManager.default.createSymbolicLink(at: symlinkFile, withDestinationURL: realFile)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
// Mount the symlink (should resolve to real file)
config.mounts.append(.share(source: symlinkFile.path, destination: "/etc/config.txt"))
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// Read the file to verify content
let readBuffer = BufferWriter()
let readExec = try await container.exec("read-file") { config in
config.arguments = ["cat", "/etc/config.txt"]
config.stdout = readBuffer
}
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")
}
guard String(data: readBuffer.data, encoding: .utf8) == initialContent else {
throw IntegrationError.assert(msg: "content mismatch on read")
}
// Write new content from container
let newContent = "modified via symlink mount"
let writeExec = try await container.exec("write-file") { config in
config.arguments = ["sh", "-c", "echo -n '\(newContent)' > /etc/config.txt"]
}
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")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
// Verify the REAL file (not symlink) was modified on the host
let hostContent = try String(contentsOf: realFile, encoding: .utf8)
guard hostContent == newContent else {
throw IntegrationError.assert(
msg: "expected '\(newContent)' in real file, got '\(hostContent)'")
}
} catch {
try? await container.stop()
throw error
}
}
}
+50
View File
@@ -872,4 +872,54 @@ extension IntegrationSuite {
throw IntegrationError.assert(msg: "expected /etc/resolv.conf to contain DNS servers, got: \(output)")
}
}
func testPodSingleFileMount() async throws {
let id = "test-pod-single-file-mount"
let bs = try await bootstrap(id)
// Create a temp file with known content
let testContent = "Hello from pod single file mount!"
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("pod-config.txt")
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
}
let buffer = 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
}
do {
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
}
guard output == testContent else {
throw IntegrationError.assert(
msg: "expected '\(testContent)', got '\(output)'")
}
} catch {
try? await pod.stop()
throw error
}
}
}
+5
View File
@@ -331,6 +331,10 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("stdin binary data", testStdinBinaryData),
Test("stdin multiple chunks", testStdinMultipleChunks),
Test("stdin very large", testStdinVeryLarge),
Test("container single file mount", testSingleFileMount),
Test("container single file mount read-only", testSingleFileMountReadOnly),
Test("container single file mount write-back", testSingleFileMountWriteBack),
Test("container single file mount symlink", testSingleFileMountSymlink),
// Pods
Test("pod single container", testPodSingleContainer),
@@ -350,6 +354,7 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("pod shared PID namespace", testPodSharedPIDNamespace),
Test("pod read-only rootfs", testPodReadOnlyRootfs),
Test("pod read-only rootfs DNS", testPodReadOnlyRootfsDNSConfigured),
Test("pod single file mount", testPodSingleFileMount),
] + macOS26Tests()
let filteredTests: [Test]