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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import PackageDescription
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.7.0"
let scVersion = "0.14.0"
let scVersion = "0.15.0"

let package = Package(
name: "container",
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContainerClient/Core/ClientImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@ extension ClientImage {
}
}

public static func pruneImages() async throws -> ([String], UInt64) {
public static func cleanupOrphanedBlobs() async throws -> ([String], UInt64) {
let client = newXPCClient()
let request = newRequest(.imagePrune)
let request = newRequest(.imageCleanupOrphanedBlobs)
let response = try await client.send(request)
let digests = try response.digests()
let size = response.uint64(key: .imageSize)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Image/ImageDelete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ extension Application {
failures.append(image.reference)
}
}
let (_, size) = try await ClientImage.pruneImages()
let (_, size) = try await ClientImage.cleanupOrphanedBlobs()
let formatter = ByteCountFormatter()
let freed = formatter.string(fromByteCount: Int64(size))

Expand Down
62 changes: 57 additions & 5 deletions Sources/ContainerCommands/Image/ImagePrune.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,76 @@

import ArgumentParser
import ContainerClient
import ContainerizationOCI
import Foundation

extension Application {
public struct ImagePrune: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "prune",
abstract: "Remove unreferenced and dangling images")
abstract: "Remove all dangling images. If -a is specified, also remove all images not referenced by any container.")

@OptionGroup
var global: Flags.Global

@Flag(name: .shortAndLong, help: "Remove all unused images, not just dangling ones")
var all: Bool = false

public func run() async throws {
let (_, size) = try await ClientImage.pruneImages()
let allImages = try await ClientImage.list()

let imagesToDelete: [ClientImage]
if all {
// Find all images not used by any container
let containers = try await ClientContainer.list()
var imagesInUse = Set<String>()
for container in containers {
imagesInUse.insert(container.configuration.image.reference)
}
imagesToDelete = allImages.filter { image in
!imagesInUse.contains(image.reference)
}
} else {
// Find dangling images (images with no tag)
imagesToDelete = allImages.filter { image in
!hasTag(image.reference)
}
}

for image in imagesToDelete {
try await ClientImage.delete(reference: image.reference, garbageCollect: false)
}

let (deletedDigests, size) = try await ClientImage.cleanupOrphanedBlobs()

let formatter = ByteCountFormatter()
let freed = formatter.string(fromByteCount: Int64(size))
print("Cleaned unreferenced images and snapshots")
print("Reclaimed \(freed) in disk space")
formatter.countStyle = .file

if imagesToDelete.isEmpty && deletedDigests.isEmpty {
print("No images to prune")
print("Reclaimed Zero KB in disk space")
} else {
print("Deleted images:")
for image in imagesToDelete {
print("untagged: \(image.reference)")
}
for digest in deletedDigests {
print("deleted: \(digest)")
}
print()
let freed = formatter.string(fromByteCount: Int64(size))
print("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
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/Helpers/Images/ImagesHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extension ImagesHelper {
routes[ImagesServiceXPCRoute.imageSave.rawValue] = harness.save
routes[ImagesServiceXPCRoute.imageLoad.rawValue] = harness.load
routes[ImagesServiceXPCRoute.imageUnpack.rawValue] = harness.unpack
routes[ImagesServiceXPCRoute.imagePrune.rawValue] = harness.prune
routes[ImagesServiceXPCRoute.imageCleanupOrphanedBlobs.rawValue] = harness.cleanupOrphanedBlobs
routes[ImagesServiceXPCRoute.imageDiskUsage.rawValue] = harness.calculateDiskUsage
routes[ImagesServiceXPCRoute.snapshotDelete.rawValue] = harness.deleteSnapshot
routes[ImagesServiceXPCRoute.snapshotGet.rawValue] = harness.getSnapshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public enum ImagesServiceXPCRoute: String {
case imageDelete
case imageSave
case imageLoad
case imagePrune
case imageCleanupOrphanedBlobs
case imageDiskUsage

case contentGet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ public actor ImagesService {
return images
}

public func prune() async throws -> ([String], UInt64) {
public func cleanupOrphanedBlobs() async throws -> ([String], UInt64) {
let images = try await self._list()
let freedSnapshotBytes = try await self.snapshotStore.clean(keepingSnapshotsFor: images)
let (deleted, freedContentBytes) = try await self.imageStore.prune()
let (deleted, freedContentBytes) = try await self.imageStore.cleanupOrphanedBlobs()
return (deleted, freedContentBytes + freedSnapshotBytes)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ public struct ImagesServiceHarness: Sendable {
}

@Sendable
public func prune(_ message: XPCMessage) async throws -> XPCMessage {
let (deleted, size) = try await service.prune()
public func cleanupOrphanedBlobs(_ message: XPCMessage) async throws -> XPCMessage {
let (deleted, size) = try await service.cleanupOrphanedBlobs()
let reply = message.reply()
let data = try JSONEncoder().encode(deleted)
reply.set(key: .digests, value: data)
Expand Down
6 changes: 3 additions & 3 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,17 +545,17 @@ container image delete [--all] [--debug] [<images> ...]

### `container image prune`

Removes unreferenced and dangling images to reclaim disk space. The command outputs the amount of space freed after deletion.
Removes unused images to reclaim disk space. By default, only removes dangling images (images with no tags). Use `-a` to remove all images not referenced by any container.

**Usage**

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

**Options**

No options.
* `-a, --all`: Remove all unused images, not just dangling ones

### `container image inspect`

Expand Down