Skip to content

Commit e7bd02f

Browse files
committed
containertool: Add special-case image source for scratch images, instead of optional source
1 parent f38a078 commit e7bd02f

File tree

4 files changed

+139
-40
lines changed

4 files changed

+139
-40
lines changed

Sources/ContainerRegistry/ImageSource.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,17 @@ public protocol ImageSource {
3535
repository: ImageReference.Repository,
3636
reference: any ImageReference.Reference
3737
) async throws -> ImageIndex
38+
39+
/// Get an image configuration record from the registry.
40+
/// - Parameters:
41+
/// - image: Reference to the image containing the record.
42+
/// - digest: Digest of the record.
43+
/// - Returns: The image confguration record stored in `repository` with digest `digest`.
44+
/// - Throws: If the blob cannot be decoded as an `ImageConfiguration`.
45+
///
46+
/// Image configuration records are stored as blobs in the registry. This function retrieves the requested blob and tries to decode it as a configuration record.
47+
func getImageConfiguration(
48+
forImage image: ImageReference,
49+
digest: ImageReference.Digest
50+
) async throws -> ImageConfiguration
3851
}

Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import class Foundation.JSONDecoder
1616

17-
extension ImageSource {
17+
extension RegistryClient {
1818
/// Get an image configuration record from the registry.
1919
/// - Parameters:
2020
/// - image: Reference to the image containing the record.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftContainerPlugin open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import struct Foundation.Data
16+
import class Foundation.JSONDecoder
17+
import class Foundation.JSONEncoder
18+
19+
public struct ScratchImageSource {
20+
var architecture: String
21+
var os: String
22+
23+
public var decoder: JSONDecoder
24+
var encoder: JSONEncoder
25+
26+
public init(architecture: String, os: String) {
27+
self.architecture = architecture
28+
self.os = os
29+
self.decoder = JSONDecoder()
30+
self.encoder = JSONEncoder()
31+
self.encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
32+
self.encoder.dateEncodingStrategy = .iso8601
33+
}
34+
}
35+
36+
extension ScratchImageSource: ImageSource {
37+
/// Fetches an unstructured blob of data from the registry.
38+
///
39+
/// - Parameters:
40+
/// - repository: Name of the repository containing the blob.
41+
/// - digest: Digest of the blob.
42+
/// - Returns: The downloaded data.
43+
/// - Throws: If the blob download fails.
44+
public func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Data {
45+
Data() // fatalError?
46+
}
47+
48+
public func getManifest(
49+
repository: ImageReference.Repository,
50+
reference: any ImageReference.Reference
51+
) async throws -> (ImageManifest, ContentDescriptor) {
52+
let config = ImageConfiguration(
53+
architecture: architecture,
54+
os: os,
55+
rootfs: .init(_type: "layers", diff_ids: [])
56+
)
57+
let encodedConfig = try encoder.encode(config)
58+
59+
let manifest = ImageManifest(
60+
schemaVersion: 2,
61+
config: ContentDescriptor(
62+
mediaType: "application/vnd.oci.image.config.v1+json",
63+
digest: "\(ImageReference.Digest(of: encodedConfig))",
64+
size: Int64(encodedConfig.count)
65+
),
66+
layers: []
67+
)
68+
let encodedManifest = try encoder.encode(manifest)
69+
70+
return (
71+
manifest,
72+
ContentDescriptor(
73+
mediaType: "application/vnd.oci.image.manifest.v1+json",
74+
digest: "\(ImageReference.Digest(of: encodedManifest))",
75+
size: Int64(encodedManifest.count)
76+
)
77+
)
78+
}
79+
80+
public func getIndex(
81+
repository: ImageReference.Repository,
82+
reference: any ImageReference.Reference
83+
) async throws -> ImageIndex {
84+
fatalError()
85+
}
86+
87+
/// Get an image configuration record from the registry.
88+
/// - Parameters:
89+
/// - image: Reference to the image containing the record.
90+
/// - digest: Digest of the record.
91+
/// - Returns: The image confguration record stored in `repository` with digest `digest`.
92+
/// - Throws: If the blob cannot be decoded as an `ImageConfiguration`.
93+
///
94+
/// Image configuration records are stored as blobs in the registry. This function retrieves the requested blob and tries to decode it as a configuration record.
95+
public func getImageConfiguration(
96+
forImage image: ImageReference,
97+
digest: ImageReference.Digest
98+
) async throws -> ImageConfiguration {
99+
ImageConfiguration(
100+
architecture: architecture,
101+
os: os,
102+
rootfs: .init(_type: "layers", diff_ids: [])
103+
)
104+
}
105+
}

Sources/containertool/containertool.swift

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
191191

192192
// The base image may be stored on a different registry to the final destination, so two clients are needed.
193193
// `scratch` is a special case and requires no source client.
194-
let source: RegistryClient?
194+
let source: ImageSource
195195
if from == "scratch" {
196-
source = nil
196+
source = ScratchImageSource(architecture: architecture, os: os)
197197
} else {
198198
source = try await RegistryClient(
199199
registry: baseImage.registry,
@@ -236,7 +236,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
236236
func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
237237
baseImage: ImageReference,
238238
destinationImage: ImageReference,
239-
source: Source?,
239+
source: Source,
240240
destination: Destination,
241241
architecture: String,
242242
os: String,
@@ -248,34 +248,17 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
248248

249249
// MARK: Find the base image
250250

251-
let baseImageManifest: ImageManifest
252-
let baseImageConfiguration: ImageConfiguration
253-
let baseImageDescriptor: ContentDescriptor
254-
if let source {
255-
(baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
256-
forImage: baseImage,
257-
architecture: architecture
258-
)
259-
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
251+
let (baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
252+
forImage: baseImage,
253+
architecture: architecture
254+
)
255+
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
260256

261-
baseImageConfiguration = try await source.getImageConfiguration(
262-
forImage: baseImage,
263-
digest: ImageReference.Digest(baseImageManifest.config.digest)
264-
)
265-
log("Found base image configuration: \(baseImageManifest.config.digest)")
266-
} else {
267-
baseImageManifest = .init(
268-
schemaVersion: 2,
269-
config: .init(mediaType: "scratch", digest: "scratch", size: 0),
270-
layers: []
271-
)
272-
baseImageConfiguration = .init(
273-
architecture: architecture,
274-
os: os,
275-
rootfs: .init(_type: "layers", diff_ids: [])
276-
)
277-
if verbose { log("Using scratch as base image") }
278-
}
257+
let baseImageConfiguration = try await source.getImageConfiguration(
258+
forImage: baseImage,
259+
digest: ImageReference.Digest(baseImageManifest.config.digest)
260+
)
261+
log("Found base image configuration: \(baseImageManifest.config.digest)")
279262

280263
// MARK: Upload resource layers
281264

@@ -357,15 +340,13 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
357340
// Copy the base image layers to the destination repository
358341
// Layers could be checked and uploaded concurrently
359342
// This could also happen in parallel with the application image build
360-
if let source {
361-
for layer in baseImageManifest.layers {
362-
try await source.copyBlob(
363-
digest: ImageReference.Digest(layer.digest),
364-
fromRepository: baseImage.repository,
365-
toClient: destination,
366-
toRepository: destinationImage.repository
367-
)
368-
}
343+
for layer in baseImageManifest.layers {
344+
try await source.copyBlob(
345+
digest: ImageReference.Digest(layer.digest),
346+
fromRepository: baseImage.repository,
347+
toClient: destination,
348+
toRepository: destinationImage.repository
349+
)
369350
}
370351

371352
// MARK: Upload application manifest

0 commit comments

Comments
 (0)