@@ -24,7 +24,7 @@ import Logging
2424import SystemPackage
2525
2626extension IntegrationSuite {
27- private func cloneRootfsForNBD ( _ rootfs: Containerization . Mount , testID: String , containerID: String ) throws -> Containerization . Mount {
27+ private func cloneRootfsForContainer ( _ rootfs: Containerization . Mount , testID: String , containerID: String ) throws -> Containerization . Mount {
2828 let clonePath = Self . testDir. appending ( component: " \( testID) - \( containerID) .ext4 " ) . absolutePath ( )
2929 try ? FileManager . default. removeItem ( atPath: clonePath)
3030 return try rootfs. clone ( to: clonePath)
@@ -295,8 +295,8 @@ extension IntegrationSuite {
295295 let ( server, diskURL) = try createNBDServer ( testID: id, name: " shared " )
296296 defer { server. stop ( ) }
297297
298- let rootfs1 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " writer " )
299- let rootfs2 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " reader " )
298+ let rootfs1 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " writer " )
299+ let rootfs2 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " reader " )
300300
301301 let pod = try LinuxPod ( id, vmm: bs. vmm) { config in
302302 config. cpus = 4
@@ -511,8 +511,8 @@ extension IntegrationSuite {
511511 let ( server, _) = try createNBDServer ( testID: id, name: " persistent " )
512512 defer { server. stop ( ) }
513513
514- let rootfs1 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " writer " )
515- let rootfs2 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " reader " )
514+ let rootfs1 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " writer " )
515+ let rootfs2 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " reader " )
516516
517517 let pod = try LinuxPod ( id, vmm: bs. vmm) { config in
518518 config. cpus = 4
@@ -578,8 +578,8 @@ extension IntegrationSuite {
578578 let ( server, _) = try createNBDServer ( testID: id, name: " shared " )
579579 defer { server. stop ( ) }
580580
581- let rootfs1 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " c1 " )
582- let rootfs2 = try cloneRootfsForNBD ( bs. rootfs, testID: id, containerID: " c2 " )
581+ let rootfs1 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " c1 " )
582+ let rootfs2 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " c2 " )
583583
584584 let pod = try LinuxPod ( id, vmm: bs. vmm) { config in
585585 config. cpus = 4
@@ -726,4 +726,130 @@ extension IntegrationSuite {
726726 }
727727 }
728728 }
729+
730+ /// Attach an empty EXT4 disk-image file as a pod volume and have
731+ /// multiple containers read from and write to the shared mount.
732+ func testPodSharedDiskImageVolume( ) async throws {
733+ let id = " test-pod-shared-disk-image-volume "
734+ let bs = try await bootstrap ( id)
735+
736+ // Create an empty EXT4 disk image to back the shared volume.
737+ let diskURL = try createEXT4DiskImage ( testID: id, name: " shared " )
738+
739+ let rootfs1 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " writer " )
740+ let rootfs2 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " appender " )
741+ let rootfs3 = try cloneRootfsForContainer ( bs. rootfs, testID: id, containerID: " reader " )
742+
743+ let pod = try LinuxPod ( id, vmm: bs. vmm) { config in
744+ config. cpus = 1
745+ config. memoryInBytes = 512 . mib ( )
746+ config. bootLog = bs. bootLog
747+ config. volumes = [
748+ . init(
749+ name: " shared-data " ,
750+ source: . diskImage( path: diskURL) ,
751+ format: " ext4 "
752+ )
753+ ]
754+ }
755+
756+ // Container 1: writes a file to the shared volume and verifies mount type.
757+ let writerBuffer = BufferWriter ( )
758+ try await pod. addContainer ( " writer " , rootfs: rootfs1) { config in
759+ config. process. arguments = [
760+ " /bin/sh " , " -c " ,
761+ " echo shared-content > /data/shared.txt && grep /data /proc/mounts " ,
762+ ]
763+ config. process. stdout = writerBuffer
764+ config. mounts. append ( . sharedMount( name: " shared-data " , destination: " /data " ) )
765+ }
766+
767+ // Container 2: reads what the writer produced and writes a second file,
768+ // mounted at a different path to prove it's the same backing store.
769+ let appenderBuffer = BufferWriter ( )
770+ try await pod. addContainer ( " appender " , rootfs: rootfs2) { config in
771+ config. process. arguments = [
772+ " /bin/sh " , " -c " ,
773+ " cat /vol/shared.txt && echo more-content > /vol/second.txt " ,
774+ ]
775+ config. process. stdout = appenderBuffer
776+ config. mounts. append ( . sharedMount( name: " shared-data " , destination: " /vol " ) )
777+ }
778+
779+ // Container 3: reads both files written by the previous containers.
780+ let readerBuffer = BufferWriter ( )
781+ try await pod. addContainer ( " reader " , rootfs: rootfs3) { config in
782+ config. process. arguments = [
783+ " /bin/sh " , " -c " ,
784+ " cat /shared/shared.txt && cat /shared/second.txt && grep /shared /proc/mounts " ,
785+ ]
786+ config. process. stdout = readerBuffer
787+ config. mounts. append ( . sharedMount( name: " shared-data " , destination: " /shared " ) )
788+ }
789+
790+ do {
791+ try await pod. create ( )
792+
793+ // Run the containers sequentially so reads see prior writes.
794+ try await pod. startContainer ( " writer " )
795+ let writerStatus = try await pod. waitContainer ( " writer " )
796+ guard writerStatus. exitCode == 0 else {
797+ throw IntegrationError . assert ( msg: " writer exited with status \( writerStatus) " )
798+ }
799+
800+ try await pod. startContainer ( " appender " )
801+ let appenderStatus = try await pod. waitContainer ( " appender " )
802+ guard appenderStatus. exitCode == 0 else {
803+ throw IntegrationError . assert ( msg: " appender exited with status \( appenderStatus) " )
804+ }
805+
806+ try await pod. startContainer ( " reader " )
807+ let readerStatus = try await pod. waitContainer ( " reader " )
808+ guard readerStatus. exitCode == 0 else {
809+ throw IntegrationError . assert ( msg: " reader exited with status \( readerStatus) " )
810+ }
811+ try await pod. stop ( )
812+ } catch {
813+ try ? await pod. stop ( )
814+ throw error
815+ }
816+
817+ // Verify writer mounted a virtio block device at /data.
818+ let writerOutput = String ( data: writerBuffer. data, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) ?? " "
819+ let writerLines = writerOutput. components ( separatedBy: " \n " )
820+ guard !writerLines. isEmpty else {
821+ throw IntegrationError . assert ( msg: " writer produced no output " )
822+ }
823+ try assertVirtioBlockMount ( writerLines. last!, path: " /data " )
824+
825+ // Verify the appender read the writer's file.
826+ let appenderOutput = String ( data: appenderBuffer. data, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) ?? " "
827+ guard appenderOutput == " shared-content " else {
828+ throw IntegrationError . assert ( msg: " appender: expected 'shared-content', got ' \( appenderOutput) ' " )
829+ }
830+
831+ // Verify the reader saw both files and a virtio block mount.
832+ let readerOutput = String ( data: readerBuffer. data, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) ?? " "
833+ let readerLines = readerOutput. components ( separatedBy: " \n " )
834+ guard readerLines. count >= 3 else {
835+ throw IntegrationError . assert ( msg: " expected at least 3 lines from reader, got: \( readerOutput) " )
836+ }
837+ guard readerLines [ 0 ] == " shared-content " else {
838+ throw IntegrationError . assert ( msg: " reader: expected 'shared-content', got ' \( readerLines [ 0 ] ) ' " )
839+ }
840+ guard readerLines [ 1 ] == " more-content " else {
841+ throw IntegrationError . assert ( msg: " reader: expected 'more-content', got ' \( readerLines [ 1 ] ) ' " )
842+ }
843+ try assertVirtioBlockMount ( readerLines [ 2 ] , path: " /shared " )
844+
845+ // Verify both writes landed on the host-side EXT4 disk image.
846+ let firstContent = try readFileFromDiskImage ( diskURL, path: " /shared.txt " )
847+ guard firstContent == " shared-content " else {
848+ throw IntegrationError . assert ( msg: " disk image /shared.txt: expected 'shared-content', got ' \( firstContent) ' " )
849+ }
850+ let secondContent = try readFileFromDiskImage ( diskURL, path: " /second.txt " )
851+ guard secondContent == " more-content " else {
852+ throw IntegrationError . assert ( msg: " disk image /second.txt: expected 'more-content', got ' \( secondContent) ' " )
853+ }
854+ }
729855}
0 commit comments