Skip to content

Commit 1c0e988

Browse files
authored
Fix container image prune to actually remove images, add -a flag support, and bump cz to 0.15.0 (#909)
- Fixes #901. ## Type of Change - [x] Bug fix - [x] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Previously `container image prune` called `ImageStore.prune()` (renamed to `cleanupOrphanedBlobs()` in cz 0.15.0) which only removed orphaned content blobs and never actually removed images. This PR fixes that behavior so `container image prune` removes dangling images by default, and with `-a` removes all unused images, not just dangling ones. ## Testing - [x] Tested locally - [ ] Added/updated tests - [ ] Added/updated docs
1 parent 7926c31 commit 1c0e988

File tree

10 files changed

+73
-21
lines changed

10 files changed

+73
-21
lines changed

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import PackageDescription
2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
2525
let builderShimVersion = "0.7.0"
26-
let scVersion = "0.14.0"
26+
let scVersion = "0.15.0"
2727

2828
let package = Package(
2929
name: "container",

Sources/ContainerClient/Core/ClientImage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,9 @@ extension ClientImage {
284284
}
285285
}
286286

287-
public static func pruneImages() async throws -> ([String], UInt64) {
287+
public static func cleanupOrphanedBlobs() async throws -> ([String], UInt64) {
288288
let client = newXPCClient()
289-
let request = newRequest(.imagePrune)
289+
let request = newRequest(.imageCleanupOrphanedBlobs)
290290
let response = try await client.send(request)
291291
let digests = try response.digests()
292292
let size = response.uint64(key: .imageSize)

Sources/ContainerCommands/Image/ImageDelete.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ extension Application {
6868
failures.append(image.reference)
6969
}
7070
}
71-
let (_, size) = try await ClientImage.pruneImages()
71+
let (_, size) = try await ClientImage.cleanupOrphanedBlobs()
7272
let formatter = ByteCountFormatter()
7373
let freed = formatter.string(fromByteCount: Int64(size))
7474

Sources/ContainerCommands/Image/ImagePrune.swift

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,76 @@
1616

1717
import ArgumentParser
1818
import ContainerClient
19+
import ContainerizationOCI
1920
import Foundation
2021

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

2829
@OptionGroup
2930
var global: Flags.Global
3031

32+
@Flag(name: .shortAndLong, help: "Remove all unused images, not just dangling ones")
33+
var all: Bool = false
34+
3135
public func run() async throws {
32-
let (_, size) = try await ClientImage.pruneImages()
36+
let allImages = try await ClientImage.list()
37+
38+
let imagesToDelete: [ClientImage]
39+
if all {
40+
// Find all images not used by any container
41+
let containers = try await ClientContainer.list()
42+
var imagesInUse = Set<String>()
43+
for container in containers {
44+
imagesInUse.insert(container.configuration.image.reference)
45+
}
46+
imagesToDelete = allImages.filter { image in
47+
!imagesInUse.contains(image.reference)
48+
}
49+
} else {
50+
// Find dangling images (images with no tag)
51+
imagesToDelete = allImages.filter { image in
52+
!hasTag(image.reference)
53+
}
54+
}
55+
56+
for image in imagesToDelete {
57+
try await ClientImage.delete(reference: image.reference, garbageCollect: false)
58+
}
59+
60+
let (deletedDigests, size) = try await ClientImage.cleanupOrphanedBlobs()
61+
3362
let formatter = ByteCountFormatter()
34-
let freed = formatter.string(fromByteCount: Int64(size))
35-
print("Cleaned unreferenced images and snapshots")
36-
print("Reclaimed \(freed) in disk space")
63+
formatter.countStyle = .file
64+
65+
if imagesToDelete.isEmpty && deletedDigests.isEmpty {
66+
print("No images to prune")
67+
print("Reclaimed Zero KB in disk space")
68+
} else {
69+
print("Deleted images:")
70+
for image in imagesToDelete {
71+
print("untagged: \(image.reference)")
72+
}
73+
for digest in deletedDigests {
74+
print("deleted: \(digest)")
75+
}
76+
print()
77+
let freed = formatter.string(fromByteCount: Int64(size))
78+
print("Reclaimed \(freed) in disk space")
79+
}
80+
}
81+
82+
private func hasTag(_ reference: String) -> Bool {
83+
do {
84+
let ref = try ContainerizationOCI.Reference.parse(reference)
85+
return ref.tag != nil && !ref.tag!.isEmpty
86+
} catch {
87+
return false
88+
}
3789
}
3890
}
3991
}

Sources/Helpers/Images/ImagesHelper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ extension ImagesHelper {
9898
routes[ImagesServiceXPCRoute.imageSave.rawValue] = harness.save
9999
routes[ImagesServiceXPCRoute.imageLoad.rawValue] = harness.load
100100
routes[ImagesServiceXPCRoute.imageUnpack.rawValue] = harness.unpack
101-
routes[ImagesServiceXPCRoute.imagePrune.rawValue] = harness.prune
101+
routes[ImagesServiceXPCRoute.imageCleanupOrphanedBlobs.rawValue] = harness.cleanupOrphanedBlobs
102102
routes[ImagesServiceXPCRoute.imageDiskUsage.rawValue] = harness.calculateDiskUsage
103103
routes[ImagesServiceXPCRoute.snapshotDelete.rawValue] = harness.deleteSnapshot
104104
routes[ImagesServiceXPCRoute.snapshotGet.rawValue] = harness.getSnapshot

Sources/Services/ContainerImagesService/Client/ImageServiceXPCRoutes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public enum ImagesServiceXPCRoute: String {
2727
case imageDelete
2828
case imageSave
2929
case imageLoad
30-
case imagePrune
30+
case imageCleanupOrphanedBlobs
3131
case imageDiskUsage
3232

3333
case contentGet

Sources/Services/ContainerImagesService/Server/ImageService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ public actor ImagesService {
118118
return images
119119
}
120120

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

Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ public struct ImagesServiceHarness: Sendable {
173173
}
174174

175175
@Sendable
176-
public func prune(_ message: XPCMessage) async throws -> XPCMessage {
177-
let (deleted, size) = try await service.prune()
176+
public func cleanupOrphanedBlobs(_ message: XPCMessage) async throws -> XPCMessage {
177+
let (deleted, size) = try await service.cleanupOrphanedBlobs()
178178
let reply = message.reply()
179179
let data = try JSONEncoder().encode(deleted)
180180
reply.set(key: .digests, value: data)

docs/command-reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,17 +545,17 @@ container image delete [--all] [--debug] [<images> ...]
545545

546546
### `container image prune`
547547

548-
Removes unreferenced and dangling images to reclaim disk space. The command outputs the amount of space freed after deletion.
548+
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.
549549

550550
**Usage**
551551

552552
```bash
553-
container image prune [--debug]
553+
container image prune [--all] [--debug]
554554
```
555555

556556
**Options**
557557

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

560560
### `container image inspect`
561561

0 commit comments

Comments
 (0)