Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
34 changes: 32 additions & 2 deletions Sources/ContainerClient/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,38 @@ public struct Utility {
volume = try await ClientVolume.inspect(parsed.name)
}
} else {
// Named volume so it must already exist
volume = try await ClientVolume.inspect(parsed.name)
// Named volume - create if doesn't exist, inspect if it does
var volumeCreated = false
do {
volume = try await ClientVolume.create(
name: parsed.name,
driver: "local",
driverOpts: [:],
labels: [:]
)
volumeCreated = true
} catch let error as VolumeError {
guard case .volumeAlreadyExists = error else {
throw error
}
// Volume already exists, just inspect it
volume = try await ClientVolume.inspect(parsed.name)
} catch let error as ContainerizationError {
// Handle XPC-wrapped volumeAlreadyExists error
guard error.message.contains("already exists") else {
throw error
}
volume = try await ClientVolume.inspect(parsed.name)
}

// Let the user know if volume was auto-created
if volumeCreated {
let warning = "Volume '\(parsed.name)' does not exist. Creating new volume."
let formattedWarning = "\u{001B}[33m\(warning)\u{001B}[0m\n"
if let warningData = formattedWarning.data(using: .utf8) {
FileHandle.standardError.write(warningData)
}
}
}

let volumeMount = Filesystem.volume(
Expand Down
31 changes: 0 additions & 31 deletions Tests/CLITests/Subcommands/Volumes/TestCLIAnonymousVolumes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -475,35 +475,4 @@ class TestCLIAnonymousVolumes: CLITest {
let afterCount = try getAnonymousVolumeNames().count
#expect(afterCount == beforeCount + 1, "anonymous volume should persist")
}

@Test func testAnonymousVolumeErrorOnNonExistentNamedVolume() throws {
let testName = getTestName()
let containerName = "\(testName)_c1"
let nonExistentVolume = "\(testName)_nonexistent"

defer {
doRemoveIfExists(name: containerName, force: true)
}

// Try to run with non-existent named volume
let (_, error, status) = try run(arguments: [
"run",
"--name",
containerName,
"-v", "\(nonExistentVolume):/data",
alpine,
"echo", "test",
])

// Should fail with volume not found error
#expect(status != 0, "should fail when named volume doesn't exist")
#expect(
error.contains("not found") || error.contains("nonexistent"),
"error should mention volume not found")

// No anonymous volume should be created
let volumes = try getAnonymousVolumeNames()
let hasMatchingVolume = volumes.contains { $0.contains(testName) }
#expect(!hasMatchingVolume, "no anonymous volume should be created for non-existent named volume")
}
}
81 changes: 81 additions & 0 deletions Tests/CLITests/Subcommands/Volumes/TestCLIVolumes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,85 @@ class TestCLIVolumes: CLITest {
// Delete volume
try doVolumeDelete(name: volumeName)
}

@Test func testImplicitNamedVolumeCreation() throws {
let testName = getTestName()
let containerName = "\(testName)_c1"
let volumeName = "\(testName)_autovolume"

defer {
doRemoveIfExists(name: containerName, force: true)
doVolumeDeleteIfExists(name: volumeName)
}

// Verify volume doesn't exist yet
let (listOutput, _, _) = try run(arguments: ["volume", "list", "--quiet"])
let volumeExistsBefore = listOutput.contains(volumeName)
#expect(!volumeExistsBefore, "volume should not exist initially")

// Run container with non-existent named volume - should auto-create
let (output, error, status) = try run(arguments: [
"run",
"--name",
containerName,
"-v", "\(volumeName):/data",
alpine,
"echo", "test",
])

// Should succeed and create volume automatically
#expect(status == 0, "should succeed and auto-create named volume")
#expect(
error.contains("does not exist. Creating new volume"),
"should warn about auto-creating volume")
#expect(output.contains("test"), "container should run successfully")

// Volume should now exist
let (listOutputAfter, _, _) = try run(arguments: ["volume", "list", "--quiet"])
let volumeExistsAfter = listOutputAfter.contains(volumeName)
#expect(volumeExistsAfter, "volume should be created")
}

@Test func testImplicitNamedVolumeReuse() throws {
let testName = getTestName()
let containerName1 = "\(testName)_c1"
let containerName2 = "\(testName)_c2"
let volumeName = "\(testName)_sharedvolume"

defer {
doRemoveIfExists(name: containerName1, force: true)
doRemoveIfExists(name: containerName2, force: true)
doVolumeDeleteIfExists(name: volumeName)
}

// First container - should auto-create volume with warning
let (_, error1, status1) = try run(arguments: [
"run",
"--name",
containerName1,
"-v", "\(volumeName):/data",
alpine,
"sh", "-c", "echo 'first' > /data/test.txt",
])

#expect(status1 == 0, "first container should succeed")
#expect(
error1.contains("does not exist. Creating new volume"),
"should warn about auto-creating volume")

// Second container - should reuse existing volume without warning
let (_, error2, status2) = try run(arguments: [
"run",
"--name",
containerName2,
"-v", "\(volumeName):/data",
alpine,
"cat", "/data/test.txt",
])

#expect(status2 == 0, "second container should succeed")
#expect(
!error2.contains("does not exist. Creating new volume"),
"should NOT warn when volume already exists")
}
}
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ Only global flags are available for debugging, version, and help.

## Volume Management

Manage persistent volumes for containers. Volumes can be explicitly created with `volume create` or implicitly created using anonymous volume syntax (`-v /path` without a source name).
Manage persistent volumes for containers. Volumes can be explicitly created with `volume create` or implicitly created when referenced in container commands (e.g., `-v myvolume:/path` or `-v /path` for anonymous volumes).

### `container volume create`

Expand Down