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
14 changes: 12 additions & 2 deletions Sources/ContainerCommands/Volume/VolumePrune.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ extension Application.VolumeCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "prune",
abstract: "Remove volumes with no container references")
abstract: "Remove anonymous volumes with no container references")

@Flag(name: .shortAndLong, help: "Remove volumes that were named too, not only anonymous ones")
var all = false

@OptionGroup
public var logOptions: Flags.Logging
Expand All @@ -43,8 +46,15 @@ extension Application.VolumeCommand {
}
}

// A volume someone named is theirs, and one nothing mounts yet is
// still theirs to mount, so a prune leaves it alone unless asked for
// all of them. A volume nobody named was made because a container
// was given one, and is of no use to anyone once nothing mounts it.
let volumesToPrune = allVolumes.filter { volume in
!volumesInUse.contains(volume.name)
guard !volumesInUse.contains(volume.name) else {
return false
}
return all || volume.isAnonymous
}

var prunedVolumes = [String]()
Expand Down
39 changes: 35 additions & 4 deletions Tests/IntegrationTests/Volumes/TestCLIVolumesSerial.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct TestCLIVolumesSerial {
let list = try f.run(["volume", "list", "--quiet"]).check().output
#expect(list.contains(v1) && list.contains(v2))

let result = try f.run(["volume", "prune"]).check()
let result = try f.run(["volume", "prune", "--all"]).check()
#expect(result.output.contains(v1))
#expect(result.output.contains(v2))
#expect(result.error.contains("Reclaimed"))
Expand All @@ -53,6 +53,37 @@ struct TestCLIVolumesSerial {
}
}

@Test func testVolumePruneTakesTheAnonymousAndKeepsTheNamed() async throws {
try await ContainerFixture.with { f in
let named = "\(f.testID)-named"
let c = "\(f.testID)-c1"
try f.doPull(alpine)
f.addCleanup {
try? f.doRemoveIfExists(c, force: true, ignoreFailure: true)
f.doVolumeDeleteIfExists(named)
}

// A container given a mount with no name is given a volume nobody
// asked for by name, which is the kind a prune is free to take.
try f.doVolumeCreate(named)
try await f.doLongRun(name: c, image: alpine, args: ["-v", "/data"], autoRemove: false, waitUntilRunning: true)
let anonymous = try f.getContainerMountedVolumeNames(c)
try #require(anonymous.count == 1, "the container should mount exactly one anonymous volume")
let anon = anonymous[0]
f.addCleanup { f.doVolumeDeleteIfExists(anon) }

try f.doStop(c)
try f.doRemove(c)

try f.run(["volume", "prune"]).check()
#expect(!(try f.volumeExists(anon)), "an anonymous volume nothing mounts should be pruned")
#expect(try f.volumeExists(named), "a named volume should survive a prune that was not asked for all")

try f.run(["volume", "prune", "--all"]).check()
#expect(!(try f.volumeExists(named)), "a named volume should be pruned when all of them are asked for")
}
}

@Test func testVolumePruneSkipsVolumeInUse() async throws {
try await ContainerFixture.with { f in
let vInUse = "\(f.testID)-inuse"
Expand All @@ -71,7 +102,7 @@ struct TestCLIVolumesSerial {
try f.doVolumeCreate(vUnused)
try await f.doLongRun(name: c, image: image, args: ["-v", "\(vInUse):/data"], autoRemove: false, waitUntilRunning: true)

try f.run(["volume", "prune"]).check()
try f.run(["volume", "prune", "--all"]).check()

let listAfter = try f.run(["volume", "list", "--quiet"]).check().output
#expect(listAfter.contains(vInUse), "in-use volume should NOT be pruned")
Expand All @@ -98,11 +129,11 @@ struct TestCLIVolumesSerial {
try f.doCreate(name: c, image: image, volumes: ["\(vol):/data"])
try await Task.sleep(for: .seconds(1))

try f.run(["volume", "prune"]).check()
try f.run(["volume", "prune", "--all"]).check()
#expect(try f.volumeExists(vol), "volume attached to stopped container should NOT be pruned")

try? f.doRemoveIfExists(c, force: true, ignoreFailure: true)
try f.run(["volume", "prune"]).check()
try f.run(["volume", "prune", "--all"]).check()
#expect(!(try f.volumeExists(vol)), "volume should be pruned after container is deleted")
}
}
Expand Down
6 changes: 3 additions & 3 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,17 +949,17 @@ container volume delete --all

### `container volume prune`

Removes all volumes that have no container references. This includes volumes that are not attached to any running or stopped containers. The command reports the actual disk space reclaimed after deletion.
Removes the anonymous volumes that have no container references, meaning those attached to no running or stopped container. A volume created with a name is kept, since it is yours to mount later, and `--all` removes those too. The command reports the actual disk space reclaimed after deletion.

**Usage**

```bash
container volume prune [--debug]
container volume prune [--all] [--debug]
```

**Options**

No options.
- `--all`, `-a`: Remove volumes that were named too, not only anonymous ones.

### `container volume list (ls)`

Expand Down
4 changes: 3 additions & 1 deletion docs/resource-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ container image prune
container image prune --all
```

Remove volumes with no container references:
Remove the anonymous volumes with no container references, and then every unused
volume including the ones you named:

```bash
container volume prune
container volume prune --all
```

Reclaim space used by the builder VM's layer cache by replacing the builder:
Expand Down
4 changes: 3 additions & 1 deletion docs/volumes.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ container volume inspect foo
A volume's image is sparse, so `sizeInBytes` reports the size the volume can grow to —
512 GiB by default — rather than the space it currently occupies on disk.

Remove every volume that has no container referencing it:
Remove the anonymous volumes that no container references. A volume you named is
yours to mount later, so it is kept unless you ask for all of them:

```bash
container volume prune
container volume prune --all
```

> [!WARNING]
Expand Down