mirror of
https://github.com/apple/container.git
synced 2026-10-13 17:49:16 +00:00
Add support for tmpfs based pod volumes (#803)
`tmpfs` based volumes can also be shared between containers in the same `LinuxPod`. This change adds support for it
This commit is contained in:
1 parent
5d5204a383
commit
d9868bb657
4 files changed
+136
-2
No files matched your search
@@ -114,6 +114,8 @@ public final class LinuxPod: Sendable {
|
||||
case nbd(url: URL, timeout: TimeInterval? = nil, readOnly: Bool = false)
|
||||
/// A disk-image file on the host, attached as a virtio-block device.
|
||||
case diskImage(path: URL, readOnly: Bool = false)
|
||||
/// An in-memory (tmpfs) volume mounted inside the guest.
|
||||
case tmpfs(sizeBytes: UInt64? = nil)
|
||||
}
|
||||
|
||||
/// The logical name of this volume. Containers reference this name
|
||||
@@ -151,6 +153,13 @@ public final class LinuxPod: Sendable {
|
||||
destination: LinuxPod.guestVolumePath(name),
|
||||
options: readOnly ? ["ro"] : []
|
||||
)
|
||||
case .tmpfs(let sizeBytes):
|
||||
return Mount.any(
|
||||
type: "tmpfs",
|
||||
source: "tmpfs",
|
||||
destination: LinuxPod.guestVolumePath(name),
|
||||
options: sizeBytes.map { ["size=\($0)"] } ?? []
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,10 +425,17 @@ extension LinuxPod {
|
||||
mount.destination = Self.guestRootfsPath(id)
|
||||
try await agent.mount(mount)
|
||||
|
||||
// Filter out shared mounts — those are handled separately as
|
||||
// pod volume bind mounts. Without it here, a container added to an
|
||||
// already-created would add a duplicated mount into the shared VM.
|
||||
let nonSharedMounts = fileMountContext.transformedMounts.filter {
|
||||
if case .shared = $0.runtimeOptions { return false }
|
||||
return true
|
||||
}
|
||||
try vm.registerMounts(
|
||||
id: id,
|
||||
rootfs: attachment,
|
||||
additionalMounts: fileMountContext.transformedMounts
|
||||
additionalMounts: nonSharedMounts
|
||||
)
|
||||
|
||||
// Mount the hotplugged container's virtiofs shares in the
|
||||
@@ -741,7 +757,7 @@ extension LinuxPod {
|
||||
type: volume.format,
|
||||
source: attachment.source,
|
||||
destination: guestPath,
|
||||
options: []
|
||||
options: attachment.options
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -853,5 +853,97 @@ extension IntegrationSuite {
|
||||
throw IntegrationError.assert(msg: "disk image /second.txt: expected 'more-content', got '\(secondContent)'")
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach an in-memory tmpfs pod volume shared across two containers: one
|
||||
/// writes a file and verifies the mount type/size, the other reads the file
|
||||
/// back from a different mount path to prove it is the same backing store.
|
||||
func testPodSharedTmpfsVolume() async throws {
|
||||
let id = "test-pod-shared-tmpfs-volume"
|
||||
let bs = try await bootstrap(id)
|
||||
|
||||
let rootfs1 = try cloneRootfsForContainer(bs.rootfs, testID: id, containerID: "writer")
|
||||
let rootfs2 = try cloneRootfsForContainer(bs.rootfs, testID: id, containerID: "reader")
|
||||
|
||||
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
|
||||
config.cpus = 1
|
||||
config.memoryInBytes = 512.mib()
|
||||
config.bootLog = bs.bootLog
|
||||
config.volumes = [
|
||||
.init(
|
||||
name: "mytmpfs",
|
||||
source: .tmpfs(sizeBytes: 128.mib()),
|
||||
format: "tmpfs"
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
// Container 1: writes a file to the shared tmpfs volume and verifies mount type.
|
||||
let writerBuffer = BufferWriter()
|
||||
try await pod.addContainer("writer", rootfs: rootfs1) { config in
|
||||
config.process.arguments = [
|
||||
"/bin/sh", "-c",
|
||||
"echo bar > /my-tmpfs-mount/foo && grep /my-tmpfs-mount /proc/mounts",
|
||||
]
|
||||
config.process.stdout = writerBuffer
|
||||
config.mounts.append(.sharedMount(name: "mytmpfs", destination: "/my-tmpfs-mount"))
|
||||
}
|
||||
|
||||
// Container 2: reads the file the writer produced, mounted at a different
|
||||
// path to prove it is the same in-memory backing store.
|
||||
let readerBuffer = BufferWriter()
|
||||
try await pod.addContainer("reader", rootfs: rootfs2) { config in
|
||||
config.process.arguments = [
|
||||
"/bin/sh", "-c",
|
||||
"cat /shared-tmpfs/foo && grep /shared-tmpfs /proc/mounts",
|
||||
]
|
||||
config.process.stdout = readerBuffer
|
||||
config.mounts.append(.sharedMount(name: "mytmpfs", destination: "/shared-tmpfs"))
|
||||
}
|
||||
|
||||
do {
|
||||
try await pod.create()
|
||||
|
||||
// Run sequentially so the reader observes the writer's file.
|
||||
try await pod.startContainer("writer")
|
||||
let writerStatus = try await pod.waitContainer("writer")
|
||||
guard writerStatus.exitCode == 0 else {
|
||||
throw IntegrationError.assert(msg: "writer exited with status \(writerStatus)")
|
||||
}
|
||||
|
||||
try await pod.startContainer("reader")
|
||||
let readerStatus = try await pod.waitContainer("reader")
|
||||
guard readerStatus.exitCode == 0 else {
|
||||
throw IntegrationError.assert(msg: "reader exited with status \(readerStatus)")
|
||||
}
|
||||
try await pod.stop()
|
||||
} catch {
|
||||
try? await pod.stop()
|
||||
throw error
|
||||
}
|
||||
|
||||
// Verify the writer mounted a tmpfs of the expected size.
|
||||
let writerOut = String(data: writerBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard writerOut.contains("tmpfs /my-tmpfs-mount tmpfs") else {
|
||||
throw IntegrationError.assert(msg: "pod volume /my-tmpfs-mount is not backed by tmpfs: \(writerOut)")
|
||||
}
|
||||
// tmpfs reports its size in /proc/mounts in kibibytes with a trailing 'k'.
|
||||
let expectedSizeKiB: UInt64 = 128 * 1024
|
||||
guard writerOut.contains("size=\(expectedSizeKiB)k") else {
|
||||
throw IntegrationError.assert(msg: "tmpfs mount is not of expected size (\(expectedSizeKiB)k): \(writerOut)")
|
||||
}
|
||||
|
||||
// Verify the reader saw the writer's file on the shared tmpfs at its own path.
|
||||
let readerOut = String(data: readerBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let readerLines = readerOut.components(separatedBy: "\n")
|
||||
guard readerLines.count >= 2 else {
|
||||
throw IntegrationError.assert(msg: "expected at least 2 lines from reader, got: \(readerOut)")
|
||||
}
|
||||
guard readerLines[0] == "bar" else {
|
||||
throw IntegrationError.assert(msg: "reader: expected 'bar', got '\(readerLines[0])'")
|
||||
}
|
||||
guard readerOut.contains("tmpfs /shared-tmpfs tmpfs") else {
|
||||
throw IntegrationError.assert(msg: "reader mount /shared-tmpfs is not backed by tmpfs: \(readerOut)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -617,6 +617,7 @@ struct IntegrationSuite: AsyncParsableCommand {
|
||||
Test("pod NBD volume identity", testPodNBDVolumeIdentity),
|
||||
Test("pod filesystem operation", testPodFilesystemOperation),
|
||||
Test("pod shared disk image volume", testPodSharedDiskImageVolume),
|
||||
Test("pod shared tmpfs volume", testPodSharedTmpfsVolume),
|
||||
] + macOS26Tests()
|
||||
let tests: [Test] = crossPlatformTests + macOSOnlyTests
|
||||
#else
|
||||
|
||||
@@ -311,6 +311,31 @@ struct PodVolumeTests {
|
||||
#expect(mount.isBlock)
|
||||
}
|
||||
|
||||
@Test func podVolumeTmpfsToMountConvertsCorrectly() {
|
||||
let volume = LinuxPod.PodVolume(
|
||||
name: "mem",
|
||||
source: .tmpfs(sizeBytes: 64 * 1024 * 1024),
|
||||
format: "tmpfs"
|
||||
)
|
||||
let mount = volume.toMount()
|
||||
#expect(mount.type == "tmpfs")
|
||||
#expect(mount.source == "tmpfs")
|
||||
#expect(mount.destination == "/run/volumes/mem")
|
||||
#expect(mount.options == ["size=\(64 * 1024 * 1024)"])
|
||||
}
|
||||
|
||||
@Test func podVolumeTmpfsWithoutSizeHasNoOptions() {
|
||||
let volume = LinuxPod.PodVolume(
|
||||
name: "mem",
|
||||
source: .tmpfs(),
|
||||
format: "tmpfs"
|
||||
)
|
||||
let mount = volume.toMount()
|
||||
#expect(mount.type == "tmpfs")
|
||||
#expect(mount.options.isEmpty)
|
||||
#expect(!mount.isBlock)
|
||||
}
|
||||
|
||||
@Test func sharedMountCreation() {
|
||||
let mount = Mount.sharedMount(
|
||||
name: "shared-data",
|
||||
|
||||
Reference in new issue
Block a user