Skip to content
Merged
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
13 changes: 4 additions & 9 deletions Sources/ContainerClient/Core/ClientVolume.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,13 @@ public struct ClientVolume {
return try JSONDecoder().decode(Volume.self, from: responseData)
}

public static func prune() async throws -> ([String], UInt64) {
public static func volumeDiskUsage(name: String) async throws -> UInt64 {
let client = XPCClient(service: serviceIdentifier)
let message = XPCMessage(route: .volumePrune)
let message = XPCMessage(route: .volumeDiskUsage)
message.set(key: .volumeName, value: name)
let reply = try await client.send(message)

guard let responseData = reply.dataNoCopy(key: .volumes) else {
return ([], 0)
}

let volumeNames = try JSONDecoder().decode([String].self, from: responseData)
let size = reply.uint64(key: .volumeSize)
return (volumeNames, size)
return size
}

}
2 changes: 1 addition & 1 deletion Sources/ContainerClient/Core/XPC+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ public enum XPCRoute: String {
case volumeDelete
case volumeList
case volumeInspect
case volumePrune

case volumeDiskUsage
case systemDiskUsage

case ping
Expand Down
46 changes: 35 additions & 11 deletions Sources/ContainerCommands/Volume/VolumePrune.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,43 @@ extension Application.VolumeCommand {
var global: Flags.Global

public func run() async throws {
let (volumeNames, size) = try await ClientVolume.prune()
let formatter = ByteCountFormatter()
let freed = formatter.string(fromByteCount: Int64(size))

if volumeNames.isEmpty {
print("No volumes to prune")
} else {
print("Pruned volumes:")
for name in volumeNames {
print(name)
let allVolumes = try await ClientVolume.list()

// Find all volumes not used by any container
let containers = try await ClientContainer.list()
var volumesInUse = Set<String>()
for container in containers {
for mount in container.configuration.mounts {
if mount.isVolume, let volumeName = mount.volumeName {
volumesInUse.insert(volumeName)
}
}
}

let volumesToPrune = allVolumes.filter { volume in
!volumesInUse.contains(volume.name)
}

var prunedVolumes = [String]()
var totalSize: UInt64 = 0

for volume in volumesToPrune {
do {
let actualSize = try await ClientVolume.volumeDiskUsage(name: volume.name)
totalSize += actualSize
try await ClientVolume.delete(name: volume.name)
prunedVolumes.append(volume.name)
} catch {
log.error("Failed to prune volume \(volume.name): \(error)")
}
print()
}

for name in prunedVolumes {
print(name)
}

let formatter = ByteCountFormatter()
let freed = formatter.string(fromByteCount: Int64(totalSize))
print("Reclaimed \(freed) in disk space")
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Helpers/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ extension APIServer {
routes[XPCRoute.volumeDelete] = harness.delete
routes[XPCRoute.volumeList] = harness.list
routes[XPCRoute.volumeInspect] = harness.inspect
routes[XPCRoute.volumePrune] = harness.prune
routes[XPCRoute.volumeDiskUsage] = harness.diskUsage

return service
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ public struct VolumesHarness: Sendable {
}

@Sendable
public func prune(_ message: XPCMessage) async throws -> XPCMessage {
let (volumeNames, size) = try await service.prune()
let data = try JSONEncoder().encode(volumeNames)
public func diskUsage(_ message: XPCMessage) async throws -> XPCMessage {
guard let name = message.string(key: .volumeName) else {
throw ContainerizationError(.invalidArgument, message: "volume name cannot be empty")
}
let size = try await service.volumeDiskUsage(name: name)

let reply = message.reply()
reply.set(key: .volumes, value: data)
reply.set(key: .volumeSize, value: size)
return reply
}
Expand Down
46 changes: 4 additions & 42 deletions Sources/Services/ContainerAPIService/Volumes/VolumesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,48 +72,10 @@ public actor VolumesService {
}
}

public func prune() async throws -> ([String], UInt64) {
try await lock.withLock { _ in
let allVolumes = try await self.store.list()

// do entire prune operation atomically with container list
return try await self.containersService.withContainerList { containers in
var inUseSet = Set<String>()
for container in containers {
for mount in container.configuration.mounts {
if mount.isVolume, let volumeName = mount.volumeName {
inUseSet.insert(volumeName)
}
}
}

let volumesToPrune = allVolumes.filter { volume in
!inUseSet.contains(volume.name)
}

var prunedNames = [String]()
var totalSize: UInt64 = 0

for volume in volumesToPrune {
do {
// calculate actual disk usage before deletion
let volumePath = self.volumePath(for: volume.name)
let actualSize = self.calculateDirectorySize(at: volumePath)

try await self.store.delete(volume.name)
try self.removeVolumeDirectory(for: volume.name)

prunedNames.append(volume.name)
totalSize += actualSize
self.log.info("Pruned volume", metadata: ["name": "\(volume.name)", "size": "\(actualSize)"])
} catch {
self.log.error("failed to prune volume \(volume.name): \(error)")
}
}

return (prunedNames, totalSize)
}
}
/// Calculate disk usage for a single volume
public func volumeDiskUsage(name: String) async throws -> UInt64 {
let volumePath = self.volumePath(for: name)
return self.calculateDirectorySize(at: volumePath)
}

/// Calculate disk usage for volumes
Expand Down
2 changes: 1 addition & 1 deletion Tests/CLITests/Subcommands/Volumes/TestCLIVolumes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ class TestCLIVolumes: CLITest {
throw CLIError.executionFailed("volume prune failed: \(error)")
}

#expect(output.contains("0 B") || output.contains("No volumes to prune"), "should show no space reclaimed or no volumes message")
#expect(output.contains("Zero KB"), "should show no space reclaimed")
}

@Test func testVolumePruneUnusedVolumes() throws {
Expand Down