diff --git a/Sources/ContainerCommands/Image/ImageResource+ListDisplayable.swift b/Sources/ContainerCommands/Image/ImageResource+ListDisplayable.swift index 9a68fde5e..33c9d59eb 100644 --- a/Sources/ContainerCommands/Image/ImageResource+ListDisplayable.swift +++ b/Sources/ContainerCommands/Image/ImageResource+ListDisplayable.swift @@ -17,10 +17,11 @@ import ContainerAPIClient import ContainerResource import ContainerizationOCI +import Foundation extension ImageResource: ListDisplayable { public static var tableHeader: [String] { - ["NAME", "TAG", "DIGEST"] + ["NAME", "TAG", "DIGEST", "DISK USAGE"] } public var tableRow: [String] { @@ -30,10 +31,23 @@ extension ImageResource: ListDisplayable { reference?.name ?? displayReference, reference?.tag ?? "", Utility.trimDigest(digest: configuration.descriptor.digest), + formattedDiskUsage, ] } public var quietValue: String { name } + + private var diskUsage: Int64 { + variants + // Skip attestation manifests, which use the `unknown/unknown` platform. + .filter { !($0.platform.os == "unknown" && $0.platform.architecture == "unknown") } + .reduce(0) { $0 + $1.size } + } + + private var formattedDiskUsage: String { + let formatter = ByteCountFormatter() + return formatter.string(fromByteCount: diskUsage) + } } diff --git a/Tests/ContainerCommandsTests/ListFormattingTests.swift b/Tests/ContainerCommandsTests/ListFormattingTests.swift index 3c0c5be8c..0755cb53d 100644 --- a/Tests/ContainerCommandsTests/ListFormattingTests.swift +++ b/Tests/ContainerCommandsTests/ListFormattingTests.swift @@ -15,6 +15,7 @@ //===----------------------------------------------------------------------===// import ContainerResource +import ContainerizationOCI import Foundation import Testing @@ -273,6 +274,58 @@ struct NetworkResourceDisplayTests { } } +// MARK: - ImageResource ListDisplayable conformance tests + +struct ImageResourceDisplayTests { + @Test + func tableHeaderIncludesDiskUsage() { + #expect(ImageResource.tableHeader == ["NAME", "TAG", "DIGEST", "DISK USAGE"]) + } + + @Test + func tableRowIncludesDiskUsage() { + let resource = makeImageResource(variants: [ + .init(platform: try! Platform(from: "linux/arm64"), digest: "sha256:manifest1", size: 1024, config: makeImageConfig()), + .init(platform: try! Platform(from: "linux/amd64"), digest: "sha256:manifest2", size: 2048, config: makeImageConfig()), + ]) + + #expect(resource.tableRow == ["alpine", "latest", "abcdef123456", formattedByteCount(3072)]) + } + + @Test + func diskUsageSkipsUnknownPlatformAttestations() { + let resource = makeImageResource(variants: [ + .init(platform: try! Platform(from: "linux/arm64"), digest: "sha256:manifest1", size: 1024, config: makeImageConfig()), + .init(platform: Platform(arch: "unknown", os: "unknown"), digest: "sha256:attestation", size: 2048, config: makeImageConfig()), + ]) + + #expect(resource.tableRow.last == formattedByteCount(1024)) + } + + private func makeImageResource(variants: [ImageResource.Variant]) -> ImageResource { + ImageResource( + configuration: .init( + description: .init( + reference: "docker.io/library/alpine:latest", + descriptor: .init(mediaType: "application/vnd.oci.image.index.v1+json", digest: "sha256:abcdef123456", size: 42) + ), + creationDate: Date(timeIntervalSince1970: 0) + ), + variants: variants, + displayReference: "alpine:latest" + ) + } + + private func makeImageConfig() -> ContainerizationOCI.Image { + .init(architecture: "arm64", os: "linux", rootfs: .init(type: "layers", diffIDs: [])) + } + + private func formattedByteCount(_ value: Int64) -> String { + let formatter = ByteCountFormatter() + return formatter.string(fromByteCount: value) + } +} + // MARK: - ListFormat tests struct ListFormatTests { diff --git a/docs/command-reference.md b/docs/command-reference.md index 1492c7f28..17119ba1c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -532,7 +532,7 @@ No options. ### `container image list (ls)` -Lists local images. Verbose output provides additional details such as image ID, creation time and full size; formatted output provides the same data in machine-readable form. +Lists local images with name, tag, digest, and disk usage. Verbose output provides additional details such as image ID, creation time and full size; formatted output provides the same data in machine-readable form. **Usage**