//===----------------------------------------------------------------------===// // Copyright © 2025-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 ArgumentParser import ContainerAPIClient import ContainerizationOCI import Foundation extension Application { public struct ImagePrune: AsyncLoggableCommand { public init() {} public static let configuration = CommandConfiguration( commandName: "prune", abstract: "Remove unused or all images") @OptionGroup public var logOptions: Flags.Logging @Flag(name: .shortAndLong, help: "Remove all unused images, not just dangling ones") var all: Bool = false public func run() async throws { let allImages = try await ClientImage.list() let imagesToPrune: [ClientImage] if all { // Find all images not used by any container let client = ContainerClient() let containers = try await client.list() var imagesInUse = Set() for container in containers { imagesInUse.insert(container.configuration.image.reference) } imagesToPrune = allImages.filter { image in !imagesInUse.contains(image.reference) } } else { // Find dangling images (images with no tag) imagesToPrune = allImages.filter { image in !hasTag(image.reference) } } var prunedImages = [String]() for image in imagesToPrune { do { try await ClientImage.delete(reference: image.reference, garbageCollect: false) prunedImages.append(image.reference) } catch { log.error( "failed to prune image", metadata: [ "ref": "\(image.reference)", "error": "\(error)", ]) } } let (deletedDigests, size) = try await ClientImage.cleanUpOrphanedBlobs() for image in imagesToPrune { print("untagged \(image.reference)") } for digest in deletedDigests { print("deleted \(digest)") } let formatter = ByteCountFormatter() formatter.countStyle = .file let freed = formatter.string(fromByteCount: Int64(size)) log.info("Reclaimed \(freed) in disk space") } private func hasTag(_ reference: String) -> Bool { do { let ref = try ContainerizationOCI.Reference.parse(reference) return ref.tag != nil && !ref.tag!.isEmpty } catch { return false } } } }