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
26 changes: 24 additions & 2 deletions Sources/ContainerCommands/Image/ImageList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension Application {
}

static func createVerboseHeader() -> [[String]] {
[["NAME", "TAG", "INDEX DIGEST", "OS", "ARCH", "VARIANT", "SIZE", "CREATED", "MANIFEST DIGEST"]]
[["NAME", "TAG", "INDEX DIGEST", "OS", "ARCH", "VARIANT", "FULL SIZE", "CREATED", "MANIFEST DIGEST"]]
}

static func printImagesVerbose(images: [ClientImage]) async throws {
Expand Down Expand Up @@ -108,7 +108,17 @@ extension Application {
}

if format == .json {
let data = try JSONEncoder().encode(images.map { $0.description })
var printableImages: [PrintableImage] = []
for image in images {
let formatter = ByteCountFormatter()
let size = try await ClientImage.getFullImageSize(image: image)
let formattedSize = formatter.string(fromByteCount: size)

printableImages.append(
PrintableImage(reference: image.reference, fullSize: formattedSize, descriptor: image.descriptor)
)
}
let data = try JSONEncoder().encode(printableImages)
print(String(data: data, encoding: .utf8)!)
return
}
Expand Down Expand Up @@ -157,6 +167,18 @@ extension Application {
}
try await printImages(images: images, format: options.format, options: options)
}

struct PrintableImage: Codable {
let reference: String
let fullSize: String
let descriptor: Descriptor

init(reference: String, fullSize: String, descriptor: Descriptor) {
self.reference = reference
self.fullSize = fullSize
self.descriptor = descriptor
}
}
}

public struct ImageList: AsyncLoggableCommand {
Expand Down
27 changes: 27 additions & 0 deletions Sources/Services/ContainerAPIService/Client/ClientImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,33 @@ extension ClientImage {
return found
}

/// Returns the total size of an image in bytes.
/// - Parameter image: The image to get the size for.
/// - Returns: The full image size in bytes.
/// - Throws: An error if the image cannot be retrieved.
public static func getFullImageSize(image: ClientImage) async throws -> Int64 {
for descriptor in try await image.index().manifests {
if let referenceType = descriptor.annotations?["vnd.docker.reference.type"],
referenceType == "attestation-manifest"
{
continue
}

guard let platform = descriptor.platform else {
continue
}

do {
let manifest = try await image.manifest(for: platform)
return
descriptor.size + manifest.config.size + manifest.layers.reduce(0) { $0 + $1.size }
} catch {
continue
}
}
return 0
}

private static func _search(reference: String, in all: [ClientImage]) throws -> ClientImage? {
let locallyBuiltImage = try {
// Check if we have an image whose index descriptor contains the image name
Expand Down
24 changes: 24 additions & 0 deletions Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,30 @@ class TestCLIImagesCommand: CLITest {
}
}

@Test func testImageFullSizeFieldExists() throws {
// 1. pull image
try doPull(imageName: alpine)

// 2. run the image ls command
let (_, output, error, status) = try run(arguments: ["image", "ls", "--format", "json"])
if status != 0 {
throw CLIError.executionFailed("failed to list images: \(error)")
}

// 3. parse the json output
guard let data = output.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]],
let image = json.first
else {
Issue.record("failed to parse JSON output or no images found: \(output)")
return
}

// 4. check that the output has a non-empty 'fullSize' field
let size = image["fullSize"] as? String ?? ""
#expect(!size.isEmpty, "expected image to have non-empty 'fullSize' field: \(image)")
}

private func addInvalidMemberToTar(tarPath: String, maliciousFilename: String) throws {
// Create a malicious entry with path traversal
let evilEntryName = "../../../../../../../../../../../tmp/\(maliciousFilename)"
Expand Down
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ No options.

### `container image list (ls)`

Lists local images. Verbose output provides additional details such as image ID, creation time and size; JSON output provides the same data in machine-readable form.
Lists local images. Verbose output provides additional details such as image ID, creation time and full size; JSON output provides the same data in machine-readable form.

**Usage**

Expand Down