Skip to content
Merged
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
46 changes: 24 additions & 22 deletions Sources/ContainerClient/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ public struct Parser {
fs.type = Filesystem.FSType.virtiofs
case "tmpfs":
fs.type = Filesystem.FSType.tmpfs
case "volume":
// Volume type will be set later in source parsing when we create the actual volume filesystem
break
default:
throw ContainerizationError(.invalidArgument, message: "unsupported mount type \(val)")
}
Expand Down Expand Up @@ -343,29 +346,28 @@ public struct Parser {
case "source":
switch type {
case "virtiofs", "bind":
// Check if it's an absolute directory path first
if val.hasPrefix("/") {
let url = URL(filePath: val)
let absolutePath = url.absoluteURL.path

var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: absolutePath, isDirectory: &isDirectory) else {
throw ContainerizationError(.invalidArgument, message: "path '\(val)' does not exist")
}
guard isDirectory.boolValue else {
throw ContainerizationError(.invalidArgument, message: "path '\(val)' is not a directory")
}
fs.source = absolutePath
} else {
guard VolumeStorage.isValidVolumeName(val) else {
throw ContainerizationError(.invalidArgument, message: "Invalid volume name '\(val)': must match \(VolumeStorage.volumeNamePattern)")
}

// This is a named volume
isVolume = true
volumeName = val
fs.source = val
// For bind mounts, resolve both absolute and relative paths
let url = URL(filePath: val)
let absolutePath = url.absoluteURL.path

var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: absolutePath, isDirectory: &isDirectory) else {
throw ContainerizationError(.invalidArgument, message: "path '\(val)' does not exist")
}
guard isDirectory.boolValue else {
throw ContainerizationError(.invalidArgument, message: "path '\(val)' is not a directory")
}
fs.source = absolutePath
case "volume":
// For volume mounts, validate as volume name
guard VolumeStorage.isValidVolumeName(val) else {
throw ContainerizationError(.invalidArgument, message: "Invalid volume name '\(val)': must match \(VolumeStorage.volumeNamePattern)")
}

// This is a named volume
isVolume = true
volumeName = val
fs.source = val
case "tmpfs":
throw ContainerizationError(.invalidArgument, message: "cannot specify source for tmpfs mount")
default:
Expand Down
103 changes: 103 additions & 0 deletions Tests/ContainerClientTests/ParserTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,107 @@ struct ParserTest {
return error.description.contains("invalid publish container port")
}
}

@Test
func testMountBindRelativePath() throws {

let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: tempDir)
}

let originalDir = FileManager.default.currentDirectoryPath
FileManager.default.changeCurrentDirectoryPath(tempDir.path)
defer {
FileManager.default.changeCurrentDirectoryPath(originalDir)
}

let result = try Parser.mount("type=bind,src=.,dst=/foo")

switch result {
case .filesystem(let fs):
let expectedPath = URL(filePath: ".").absoluteURL.path
#expect(fs.source == expectedPath)
#expect(fs.destination == "/foo")
#expect(!fs.isVolume)
case .volume:
#expect(Bool(false), "Expected filesystem mount, got volume")
}
}

@Test
func testMountBindAbsolutePath() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-abs-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: tempDir)
}

let result = try Parser.mount("type=bind,src=\(tempDir.path),dst=/foo")

switch result {
case .filesystem(let fs):
#expect(fs.source == tempDir.path)
#expect(fs.destination == "/foo")
#expect(!fs.isVolume)
case .volume:
#expect(Bool(false), "Expected filesystem mount, got volume")
}
}

@Test
func testMountVolumeValidName() throws {
let result = try Parser.mount("type=volume,src=myvolume,dst=/data")

switch result {
case .filesystem:
#expect(Bool(false), "Expected volume mount, got filesystem")
case .volume(let vol):
#expect(vol.name == "myvolume")
#expect(vol.destination == "/data")
}
}

@Test
func testMountVolumeInvalidName() throws {
#expect {
_ = try Parser.mount("type=volume,src=.,dst=/data")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("Invalid volume name")
}
}

@Test
func testMountBindNonExistentPath() throws {
#expect {
_ = try Parser.mount("type=bind,src=/nonexistent/path,dst=/foo")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("path") && error.description.contains("does not exist")
}
}

@Test
func testMountBindFileInsteadOfDirectory() throws {
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-file-\(UUID().uuidString)")
try "test content".write(to: tempFile, atomically: true, encoding: .utf8)
defer {
try? FileManager.default.removeItem(at: tempFile)
}

#expect {
_ = try Parser.mount("type=bind,src=\(tempFile.path),dst=/foo")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("path") && error.description.contains("is not a directory")
}
}
}