Skip to content

Commit 55f1e4f

Browse files
Create ImageResource conforming to ManagedResource (apple#1619)
- Closes apple#1625. - For now, the ImageResource compliance happens at the CLI level. We will work on pushing that into or closer to the APIServer at a later time. ## Testing - [x] Tested locally --------- Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
1 parent 7358102 commit 55f1e4f

8 files changed

Lines changed: 338 additions & 159 deletions

File tree

Sources/ContainerCommands/Image/ImageInspect.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ extension Application {
5050
)
5151
}
5252

53-
var printable: [ImageDetail] = []
53+
var printable: [ImageResource] = []
5454
for image in result.images {
5555
guard
5656
!Utility.isInfraImage(
@@ -59,10 +59,15 @@ extension Application {
5959
initImage: containerSystemConfig.vminit.image
6060
)
6161
else { continue }
62-
printable.append(try await image.details())
62+
let resolved = try await image.resolvedManifests()
63+
printable.append(ImageResource(config: image.description, index: resolved.index, manifests: resolved.manifests))
6364
}
6465

65-
try Output.emit(Output.renderJSON(printable))
66+
let options = JSONOptions(
67+
outputFormatting: [.prettyPrinted, .sortedKeys],
68+
dateEncodingStrategy: .iso8601
69+
)
70+
try Output.emit(Output.renderJSON(printable, options: options))
6671
}
6772
}
6873
}

Sources/ContainerCommands/Image/ImageList.swift

Lines changed: 53 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import ArgumentParser
1818
import ContainerAPIClient
1919
import ContainerPersistence
2020
import ContainerPlugin
21+
import ContainerResource
2122
import Containerization
2223
import ContainerizationError
2324
import ContainerizationOCI
2425
import Foundation
25-
import SwiftProtobuf
2626

2727
extension Application {
2828
public struct ImageList: AsyncLoggableCommand {
@@ -53,27 +53,30 @@ extension Application {
5353
}
5454
images.sort { $0.reference < $1.reference }
5555

56-
if format == .json {
57-
try await Self.emitJSON(images: images)
58-
return
59-
}
60-
61-
if quiet {
56+
// Quiet mode prints references directly and skips the more expensive
57+
// per-image manifest resolution. `--format json` takes precedence.
58+
if quiet && format != .json {
6259
for image in images {
6360
let processedReferenceString = try ClientImage.denormalizeReference(image.reference, containerSystemConfig: containerSystemConfig)
6461
print(processedReferenceString)
6562
}
6663
return
6764
}
6865

66+
let resources = try await Self.buildResources(images: images, containerSystemConfig: containerSystemConfig)
67+
68+
if format == .json {
69+
try Self.emitJSON(resources: resources)
70+
return
71+
}
72+
6973
if verbose {
70-
let items = try await Self.buildVerboseItems(images: images, containerSystemConfig: containerSystemConfig)
71-
Output.emit(Output.renderTable(items))
74+
let rows = resources.flatMap { VerboseImageRow.rows(for: $0) }
75+
Output.emit(Output.renderTable(rows))
7276
return
7377
}
7478

75-
let items = try await Self.buildTableItems(images: images, containerSystemConfig: containerSystemConfig)
76-
Output.emit(Output.renderTable(items))
79+
Output.emit(Output.renderTable(resources))
7780
}
7881

7982
private static func validate(quiet: Bool, verbose: Bool) throws {
@@ -82,111 +85,27 @@ extension Application {
8285
}
8386
}
8487

85-
private static func emitJSON(images: [ClientImage]) async throws {
86-
let formatter = ByteCountFormatter()
87-
var printableImages: [PrintableImage] = []
88+
/// Builds the resource for each image, denormalizing the reference so the
89+
/// display name omits the default registry.
90+
private static func buildResources(images: [ClientImage], containerSystemConfig: ContainerSystemConfig) async throws -> [ImageResource] {
91+
var resources: [ImageResource] = []
8892
for image in images {
89-
let size = try await ClientImage.getFullImageSize(image: image)
90-
let formattedSize = formatter.string(fromByteCount: size)
91-
printableImages.append(
92-
PrintableImage(reference: image.reference, fullSize: formattedSize, descriptor: image.descriptor)
93-
)
94-
}
95-
try Output.emit(Output.renderJSON(printableImages))
96-
}
97-
98-
private static func buildTableItems(images: [ClientImage], containerSystemConfig: ContainerSystemConfig) async throws -> [ImageRow] {
99-
var items: [ImageRow] = []
100-
for image in images {
101-
let processedReferenceString = try ClientImage.denormalizeReference(image.reference, containerSystemConfig: containerSystemConfig)
102-
let reference = try ContainerizationOCI.Reference.parse(processedReferenceString)
103-
let digest = try await image.resolved().digest
104-
items.append(
105-
ImageRow(
106-
name: reference.name,
107-
tag: reference.tag ?? "<none>",
108-
trimmedDigest: Utility.trimDigest(digest: digest)
109-
))
110-
}
111-
return items
112-
}
113-
114-
private static func buildVerboseItems(images: [ClientImage], containerSystemConfig: ContainerSystemConfig) async throws -> [VerboseImageRow] {
115-
let formatter = ByteCountFormatter()
116-
var items: [VerboseImageRow] = []
117-
for image in images {
118-
let imageDigest = try await image.resolved().digest
119-
let processedReferenceString = try ClientImage.denormalizeReference(image.reference, containerSystemConfig: containerSystemConfig)
120-
let reference = try ContainerizationOCI.Reference.parse(processedReferenceString)
121-
for descriptor in try await image.index().manifests {
122-
if let referenceType = descriptor.annotations?["vnd.docker.reference.type"],
123-
referenceType == "attestation-manifest"
124-
{
125-
continue
126-
}
127-
128-
guard let platform = descriptor.platform else {
129-
continue
130-
}
131-
132-
var config: ContainerizationOCI.Image
133-
var manifest: ContainerizationOCI.Manifest
134-
do {
135-
config = try await image.config(for: platform)
136-
manifest = try await image.manifest(for: platform)
137-
} catch {
138-
continue
139-
}
140-
141-
let created = config.created ?? ""
142-
let size = descriptor.size + manifest.config.size + manifest.layers.reduce(0) { $0 + $1.size }
143-
let formattedSize = formatter.string(fromByteCount: size)
144-
145-
items.append(
146-
VerboseImageRow(
147-
name: reference.name,
148-
tag: reference.tag ?? "<none>",
149-
indexDigest: Utility.trimDigest(digest: imageDigest),
150-
os: platform.os,
151-
arch: platform.architecture,
152-
variant: platform.variant ?? "",
153-
fullSize: formattedSize,
154-
created: created,
155-
manifestDigest: Utility.trimDigest(digest: descriptor.digest)
156-
))
157-
}
93+
let resolved = try await image.resolvedManifests()
94+
let displayReference = try ClientImage.denormalizeReference(image.reference, containerSystemConfig: containerSystemConfig)
95+
resources.append(
96+
ImageResource(config: image.description, index: resolved.index, manifests: resolved.manifests, displayReference: displayReference))
15897
}
159-
return items
98+
return resources
16099
}
161100

162-
struct PrintableImage: Codable {
163-
let reference: String
164-
let fullSize: String
165-
let descriptor: Descriptor
101+
private static func emitJSON(resources: [ImageResource]) throws {
102+
let options = JSONOptions(dateEncodingStrategy: .iso8601)
103+
try Output.emit(Output.renderJSON(resources, options: options))
166104
}
167105
}
168106
}
169107

170-
private struct ImageRow: ListDisplayable {
171-
let name: String
172-
let tag: String
173-
let trimmedDigest: String
174-
175-
static var tableHeader: [String] {
176-
["NAME", "TAG", "DIGEST"]
177-
}
178-
179-
var tableRow: [String] {
180-
[name, tag, trimmedDigest]
181-
}
182-
183-
// Required by ListDisplayable but unused — ImageList handles quiet mode
184-
// separately to avoid expensive digest resolution.
185-
var quietValue: String {
186-
name
187-
}
188-
}
189-
108+
/// A single row of the verbose image listing — one per platform variant.
190109
private struct VerboseImageRow: ListDisplayable {
191110
let name: String
192111
let tag: String
@@ -209,4 +128,30 @@ private struct VerboseImageRow: ListDisplayable {
209128
var quietValue: String {
210129
name
211130
}
131+
132+
/// Flattens an ImageResource into one verbose image row entry per platform variant.
133+
static func rows(for resource: ImageResource) -> [VerboseImageRow] {
134+
let formatter = ByteCountFormatter()
135+
let reference = try? ContainerizationOCI.Reference.parse(resource.displayReference)
136+
let name = reference?.name ?? resource.displayReference
137+
let tag = reference?.tag ?? "<none>"
138+
let indexDigest = Utility.trimDigest(digest: resource.index.digest)
139+
return
140+
resource.variants
141+
// Skip attestation manifests, which use the `unknown/unknown` platform.
142+
.filter { !($0.platform.os == "unknown" && $0.platform.architecture == "unknown") }
143+
.map { variant in
144+
VerboseImageRow(
145+
name: name,
146+
tag: tag,
147+
indexDigest: indexDigest,
148+
os: variant.platform.os,
149+
arch: variant.platform.architecture,
150+
variant: variant.platform.variant ?? "",
151+
fullSize: formatter.string(fromByteCount: variant.size),
152+
created: variant.config.created ?? "",
153+
manifestDigest: Utility.trimDigest(digest: variant.digest)
154+
)
155+
}
156+
}
212157
}

Sources/ContainerResource/Image/ImageDetail.swift renamed to Sources/ContainerCommands/Image/ImageResource+ListDisplayable.swift

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,26 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17-
import Containerization
17+
import ContainerAPIClient
18+
import ContainerResource
1819
import ContainerizationOCI
1920

20-
public struct ImageDetail: Codable {
21-
public let name: String
22-
public let index: Descriptor
23-
public let variants: [Variants]
24-
25-
public struct Variants: Codable {
26-
public let platform: Platform
27-
public let config: ContainerizationOCI.Image
28-
public let size: Int64
21+
extension ImageResource: ListDisplayable {
22+
public static var tableHeader: [String] {
23+
["NAME", "TAG", "DIGEST"]
24+
}
2925

30-
public init(platform: Platform, size: Int64, config: ContainerizationOCI.Image) {
31-
self.platform = platform
32-
self.config = config
33-
self.size = size
34-
}
26+
public var tableRow: [String] {
27+
// `displayReference` is already denormalized by the caller.
28+
let reference = try? ContainerizationOCI.Reference.parse(displayReference)
29+
return [
30+
reference?.name ?? displayReference,
31+
reference?.tag ?? "<none>",
32+
Utility.trimDigest(digest: index.digest),
33+
]
3534
}
3635

37-
public init(name: String, index: Descriptor, variants: [Variants]) {
38-
self.name = name
39-
self.index = index
40-
self.variants = variants
36+
public var quietValue: String {
37+
name
4138
}
4239
}

0 commit comments

Comments
 (0)