Skip to content
Open
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
2 changes: 1 addition & 1 deletion Sources/Container-Compose/Commands/ComposeUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
private func configVolume(_ volume: String, from dockerCompose: DockerCompose) async throws -> [String] {
try composeVolumeToRunArgs(
volume,
cwd: cwd,
composeDirectory: composeDirectory,
fileManager: fileManager,
environmentVariables: environmentVariables,
projectName: projectName,
Expand Down
4 changes: 2 additions & 2 deletions Sources/Container-Compose/Helper Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public func composePortToRunArg(_ portSpec: String) -> String {
/// Internal so tests can reach it via `@testable import ContainerComposeCore`.
func composeVolumeToRunArgs(
_ volume: String,
cwd: String,
composeDirectory: String,
fileManager: FileManager = .default,
environmentVariables: [String: String] = [:],
projectName: String?,
Expand All @@ -217,7 +217,7 @@ func composeVolumeToRunArgs(
}

if source.contains("/") || source.starts(with: ".") || source.starts(with: "..") {
let fullHostPath = resolvedPath(for: source, relativeTo: URL(fileURLWithPath: cwd, isDirectory: true))
let fullHostPath = resolvedPath(for: source, relativeTo: URL(fileURLWithPath: composeDirectory, isDirectory: true))

if fileManager.fileExists(atPath: fullHostPath) {
args.append("-v")
Expand Down
44 changes: 43 additions & 1 deletion Tests/Container-Compose-DynamicTests/ComposeUpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,49 @@ struct ComposeUpTests {

try? await stopInstance(location: tempLocation.deletingLastPathComponent())
}


@Test("Relative bind mount resolves from the compose file when up runs from the project root")
func testRelativeBindMountUsesComposeFileDirectory() async throws {
let projectRoot = URL.temporaryDirectory.appending(path: makeContainerName())
let composeDirectory = projectRoot.appending(path: "deploy")
let dataDirectory = projectRoot.appending(path: "data")
try FileManager.default.createDirectory(at: composeDirectory, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dataDirectory, withIntermediateDirectories: true)
try "mounted".write(to: dataDirectory.appending(path: "marker.txt"), atomically: true, encoding: .utf8)

let yaml = """
services:
app:
image: alpine:3.20
command: ["sleep", "3600"]
volumes: ["../data:/data"]
healthcheck:
test: ["CMD-SHELL", "test \"$(cat /data/marker.txt)\" = mounted"]
interval: "1s"
timeout: "2s"
retries: 5
"""
try yaml.write(to: composeDirectory.appending(path: "docker-compose.yaml"), atomically: true, encoding: .utf8)

let arguments = ["--cwd", projectRoot.path, "-f", "deploy/docker-compose.yaml"]
do {
var composeUp = try ComposeUp.parse(["-d"] + arguments)
try await composeUp.run()

let container = try await ContainerClient().get(id: "\(projectRoot.lastPathComponent)-app")
#expect(container.status == .running)

var composeDown = try ComposeDown.parse(arguments)
try await composeDown.run()
} catch {
var composeDown = try? ComposeDown.parse(arguments)
try? await composeDown?.run()
try? FileManager.default.removeItem(at: projectRoot)
throw error
}
try? FileManager.default.removeItem(at: projectRoot)
}

// TODO: Reenable
// @Test("Test three-tier web application with multiple networks")
// func testThreeTierWebAppWithNetworks() async throws {
Expand Down
50 changes: 35 additions & 15 deletions Tests/Container-Compose-StaticTests/HelperFunctionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ struct ComposeVolumeTests {

let result = try composeVolumeToRunArgs(
"\(hostFile.path):/app/config.yaml:ro",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(hostFile.path):/app/config.yaml:ro"])
Expand All @@ -177,7 +177,7 @@ struct ComposeVolumeTests {

let result = try composeVolumeToRunArgs(
"\(hostFile.path):/docker-entrypoint-initdb.d/init.sh",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(hostFile.path):/docker-entrypoint-initdb.d/init.sh"])
Expand All @@ -191,7 +191,7 @@ struct ComposeVolumeTests {

let result = try composeVolumeToRunArgs(
"\(dataDir.path):/app/data",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(dataDir.path):/app/data"])
Expand All @@ -205,26 +205,46 @@ struct ComposeVolumeTests {

let result = try composeVolumeToRunArgs(
"\(dataDir.path):/app/data:ro",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(dataDir.path):/app/data:ro"])
}

@Test("Relative file bind mount resolved to absolute path against cwd", .tempDir)
func testRelativeFileMountResolvedAgainstCwd() throws {
@Test("Relative file bind mount resolves to an absolute path against the compose directory", .tempDir)
func testRelativeFileMountResolvedAgainstComposeDirectory() throws {
let tmp = TempDirTrait.current
let hostFile = tmp.appending(path: "config.yaml")
FileManager.default.createFile(atPath: hostFile.path, contents: nil)

let result = try composeVolumeToRunArgs(
"./config.yaml:/app/config.yaml:ro",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(hostFile.path):/app/config.yaml:ro"])
}

@Test("Relative parent-path bind mount resolves from the compose directory", .tempDir)
func testRelativeParentPathMountResolvesFromComposeDirectory() throws {
let tmp = TempDirTrait.current
let projectDirectory = tmp.appending(path: "project")
let composeDirectory = projectDirectory.appending(path: "deploy")
let dataDirectory = projectDirectory.appending(path: "data")
let markerFile = dataDirectory.appending(path: "marker.txt")
try FileManager.default.createDirectory(at: composeDirectory, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dataDirectory, withIntermediateDirectories: true)
FileManager.default.createFile(atPath: markerFile.path, contents: nil)

let result = try composeVolumeToRunArgs(
"../data:/data",
composeDirectory: composeDirectory.path,
projectName: "test"
)

#expect(result == ["-v", "\(dataDirectory.path):/data"])
}

@Test("Missing host path is auto-created as a directory", .tempDir)
func testMissingHostPathAutoCreated() throws {
let tmp = TempDirTrait.current
Expand All @@ -233,7 +253,7 @@ struct ComposeVolumeTests {

let result = try composeVolumeToRunArgs(
"\(newDir.path):/app/data",
cwd: tmp.path,
composeDirectory: tmp.path,
projectName: "test"
)
#expect(result == ["-v", "\(newDir.path):/app/data"])
Expand All @@ -244,15 +264,15 @@ struct ComposeVolumeTests {

@Test("Invalid volume format returns empty array")
func testInvalidFormatReturnsEmpty() throws {
let result = try composeVolumeToRunArgs("nodestination", cwd: "/tmp", projectName: "test")
let result = try composeVolumeToRunArgs("nodestination", composeDirectory: "/tmp", projectName: "test")
#expect(result == [])
}

@Test("Named volume is forwarded using project-scoped native container volume syntax")
func testNamedVolumeUsesNativeVolumeSyntax() throws {
let result = try composeVolumeToRunArgs(
"data:/var/lib/postgresql/data",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "test"
)
#expect(result == ["-v", "test_data:/var/lib/postgresql/data"])
Expand All @@ -262,7 +282,7 @@ struct ComposeVolumeTests {
func testNamedVolumeMountsAtDeclaredSingleSegmentDestination() throws {
let result = try composeVolumeToRunArgs(
"redisdata:/data",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "proj"
)
#expect(result == ["-v", "proj_redisdata:/data"])
Expand All @@ -272,7 +292,7 @@ struct ComposeVolumeTests {
func testNamedVolumePreservesNestedDestinationAndMode() throws {
let result = try composeVolumeToRunArgs(
"pgdata:/var/lib/postgresql/data:ro",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "proj"
)
#expect(result == ["-v", "proj_pgdata:/var/lib/postgresql/data:ro"])
Expand All @@ -282,7 +302,7 @@ struct ComposeVolumeTests {
func testNamedVolumeUsesExplicitTopLevelName() throws {
let result = try composeVolumeToRunArgs(
"db-data:/var/lib/postgresql/data:ro",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "test",
volumeDefinitions: [
"db-data": Volume(name: "prod-db-data")
Expand All @@ -295,7 +315,7 @@ struct ComposeVolumeTests {
func testNamedVolumeKeepsExternalSourceName() throws {
let result = try composeVolumeToRunArgs(
"db-data:/var/lib/postgresql/data",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "test",
volumeDefinitions: [
"db-data": Volume(external: ExternalVolume(isExternal: true, name: nil))
Expand All @@ -308,7 +328,7 @@ struct ComposeVolumeTests {
func testNamedVolumeUsesExplicitExternalName() throws {
let result = try composeVolumeToRunArgs(
"db-data:/var/lib/postgresql/data",
cwd: "/tmp",
composeDirectory: "/tmp",
projectName: "test",
volumeDefinitions: [
"db-data": Volume(external: ExternalVolume(isExternal: true, name: "shared-db-data"))
Expand Down