diff --git a/Sources/ContainerCommands/Volume/VolumePrune.swift b/Sources/ContainerCommands/Volume/VolumePrune.swift index cad239158..cccf00ae7 100644 --- a/Sources/ContainerCommands/Volume/VolumePrune.swift +++ b/Sources/ContainerCommands/Volume/VolumePrune.swift @@ -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 @@ -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]() diff --git a/Tests/IntegrationTests/Volumes/TestCLIVolumesSerial.swift b/Tests/IntegrationTests/Volumes/TestCLIVolumesSerial.swift index d0aa799c2..fb50076f1 100644 --- a/Tests/IntegrationTests/Volumes/TestCLIVolumesSerial.swift +++ b/Tests/IntegrationTests/Volumes/TestCLIVolumesSerial.swift @@ -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")) @@ -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" @@ -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") @@ -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") } } diff --git a/docs/command-reference.md b/docs/command-reference.md index d75ab5970..ab66e2e0d 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -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)` diff --git a/docs/resource-usage.md b/docs/resource-usage.md index cb88c7e5b..2fec83b08 100644 --- a/docs/resource-usage.md +++ b/docs/resource-usage.md @@ -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: diff --git a/docs/volumes.md b/docs/volumes.md index e1b37547e..ffc05b6eb 100644 --- a/docs/volumes.md +++ b/docs/volumes.md @@ -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]