Implicitly create named volumes (#769)

- Closes #690.

## Type of Change
- [ ] Bug fix
- [x] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
Named volumes are now implicitly created when referenced in container
commands. So, if `myvolume` doesn't exist and you run `container run -v
myvolume:/data alpine`, it is automatically created.

## Testing
- [x] Tested locally
- [x] Added/updated tests
- [x] Added/updated docs
This commit is contained in:
Raj
2025-10-16 14:48:57 -07:00
committed by GitHub
parent f90f67ccf0
commit 2f8de3a660
4 changed files with 106 additions and 61 deletions
+33 -29
View File
@@ -160,35 +160,7 @@ public struct Utility {
case .filesystem(let fs):
resolvedMounts.append(fs)
case .volume(let parsed):
let volume: Volume
if parsed.isAnonymous {
// Anonymous volume so try to create it, inspect if already exists
do {
volume = try await ClientVolume.create(
name: parsed.name,
driver: "local",
driverOpts: [:],
labels: [Volume.anonymousLabel: ""]
)
} 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)
}
} else {
// Named volume so it must already exist
volume = try await ClientVolume.inspect(parsed.name)
}
let volume = try await getOrCreateVolume(parsed: parsed)
let volumeMount = Filesystem.volume(
name: parsed.name,
format: volume.format,
@@ -305,4 +277,36 @@ public struct Utility {
}
return result
}
/// Gets an existing volume or creates it if it doesn't exist.
/// Shows a warning for named volumes when auto-creating.
private static func getOrCreateVolume(parsed: ParsedVolume) async throws -> Volume {
let labels = parsed.isAnonymous ? [Volume.anonymousLabel: ""] : [:]
let volume: Volume
do {
volume = try await ClientVolume.create(
name: parsed.name,
driver: "local",
driverOpts: [:],
labels: labels
)
} 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)
}
// TODO: Warn user if named volume was auto-created
return volume
}
}
@@ -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")
}
}
@@ -251,4 +251,76 @@ 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, _, 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(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
let (_, _, 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")
// Second container - should reuse existing volume
let (_, _, status2) = try run(arguments: [
"run",
"--name",
containerName2,
"-v", "\(volumeName):/data",
alpine,
"cat", "/data/test.txt",
])
#expect(status2 == 0, "second container should succeed")
}
}
+1 -1
View File
@@ -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`