diff --git a/Sources/ContainerCommands/Container/ContainerPrune.swift b/Sources/ContainerCommands/Container/ContainerPrune.swift index 13bfbe7f8..5e0c2a699 100644 --- a/Sources/ContainerCommands/Container/ContainerPrune.swift +++ b/Sources/ContainerCommands/Container/ContainerPrune.swift @@ -16,8 +16,6 @@ import ArgumentParser import ContainerAPIClient -import ContainerResource -import ContainerizationError import Foundation extension Application { @@ -34,32 +32,21 @@ extension Application { public func run() async throws { let client = ContainerClient() - let filters = ContainerListFilters(status: .stopped).withoutMachines() - let containersToPrune = try await client.list(filters: filters) - - var prunedContainerIds = [String]() - var totalSize: UInt64 = 0 - - for container in containersToPrune { - do { - let actualSize = try await client.diskUsage(id: container.id) - totalSize += actualSize - try await client.delete(id: container.id) - prunedContainerIds.append(container.id) - } catch { - log.error( - "failed to prune container", - metadata: [ - "id": "\(container.id)", - "error": "\(error)", - ]) - } + let result = try await client.prune() + + for failure in result.failed { + log.error( + "failed to prune container", + metadata: [ + "id": "\(failure.id)", + "error": "\(failure.error)", + ]) } let formatter = ByteCountFormatter() - let freed = formatter.string(fromByteCount: Int64(totalSize)) + let freed = formatter.string(fromByteCount: Int64(result.reclaimedBytes)) - for name in prunedContainerIds { + for name in result.pruned { print(name) } log.info("Reclaimed \(freed) in disk space") diff --git a/Sources/Services/ContainerAPIService/Client/ContainerClient+Prune.swift b/Sources/Services/ContainerAPIService/Client/ContainerClient+Prune.swift new file mode 100644 index 000000000..343dcc2ba --- /dev/null +++ b/Sources/Services/ContainerAPIService/Client/ContainerClient+Prune.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerResource + +extension ContainerClient { + /// Remove all stopped containers, returning the outcome of the operation. + public func prune() async throws -> PruneResult { + let filters = ContainerListFilters(status: .stopped).withoutMachines() + let containersToPrune = try await list(filters: filters) + var prunedContainerIds = [String]() + var failed = [PruneResult.Failure]() + var totalSize: UInt64 = 0 + + for container in containersToPrune { + do { + let actualSize = try await diskUsage(id: container.id) + totalSize += actualSize + try await delete(id: container.id) + prunedContainerIds.append(container.id) + } catch { + failed.append(PruneResult.Failure(id: container.id, error: error)) + } + } + + return PruneResult(pruned: prunedContainerIds, failed: failed, reclaimedBytes: totalSize) + } +} \ No newline at end of file diff --git a/Sources/Services/ContainerAPIService/Client/PruneResult.swift b/Sources/Services/ContainerAPIService/Client/PruneResult.swift new file mode 100644 index 000000000..224990f62 --- /dev/null +++ b/Sources/Services/ContainerAPIService/Client/PruneResult.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// The outcome of a prune operation on containers, images, volumes, or networks. +public struct PruneResult: Sendable { + public struct Failure: Sendable { + public let id: String + public let error: any Error + + public init(id: String, error: any Error) { + self.id = id + self.error = error + } + } + + public let pruned: [String] + + public let failed: [Failure] + + public let reclaimedBytes: UInt64 + + public init(pruned: [String], failed: [Failure], reclaimedBytes: UInt64) { + self.pruned = pruned + self.failed = failed + self.reclaimedBytes = reclaimedBytes + } +} \ No newline at end of file