Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions Sources/Containerization/FileMount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Containerization/LinuxPod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [
Expand Down
28 changes: 23 additions & 5 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion Sources/Integration/PodTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading