Skip to content

Commit dfa75ac

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

File tree

3 files changed

+140
-39
lines changed

3 files changed

+140
-39
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
/// ScratchImage is a special-purpose ImageSource which represents the scratch image.
20+
public struct ScratchImage {
21+
var architecture: String
22+
var os: String
23+
24+
var decoder: JSONDecoder
25+
var encoder: JSONEncoder
26+
27+
public init(architecture: String, os: String) {
28+
self.architecture = architecture
29+
self.os = os
30+
self.decoder = JSONDecoder()
31+
self.encoder = JSONEncoder()
32+
self.encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
33+
self.encoder.dateEncodingStrategy = .iso8601
34+
}
35+
}
36+
37+
extension ScratchImage: ImageSource {
38+
/// Fetches an unstructured blob of data from the registry.
39+
///
40+
/// - Parameters:
41+
/// - repository: Name of the repository containing the blob.
42+
/// - digest: Digest of the blob.
43+
/// - Returns: The downloaded data.
44+
/// - Throws: If the blob download fails.
45+
public func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Data {
46+
Data() // fatalError?
47+
}
48+
49+
/// Fetches an image manifest.
50+
///
51+
/// - Parameters:
52+
/// - repository: Name of the source repository.
53+
/// - reference: Tag or digest of the manifest to fetch.
54+
/// - Returns: The downloaded manifest.
55+
/// - Throws: If the download fails or the manifest cannot be decoded.
56+
public func getManifest(
57+
repository: ImageReference.Repository,
58+
reference: any ImageReference.Reference
59+
) async throws -> (ImageManifest, ContentDescriptor) {
60+
let config = ImageConfiguration(
61+
architecture: architecture,
62+
os: os,
63+
rootfs: .init(_type: "layers", diff_ids: [])
64+
)
65+
let encodedConfig = try encoder.encode(config)
66+
67+
let manifest = ImageManifest(
68+
schemaVersion: 2,
69+
config: ContentDescriptor(
70+
mediaType: "application/vnd.oci.image.config.v1+json",
71+
digest: "\(ImageReference.Digest(of: encodedConfig))",
72+
size: Int64(encodedConfig.count)
73+
),
74+
layers: []
75+
)
76+
let encodedManifest = try encoder.encode(manifest)
77+
78+
return (
79+
manifest,
80+
ContentDescriptor(
81+
mediaType: "application/vnd.oci.image.manifest.v1+json",
82+
digest: "\(ImageReference.Digest(of: encodedManifest))",
83+
size: Int64(encodedManifest.count)
84+
)
85+
)
86+
}
87+
88+
/// Fetches an image index.
89+
///
90+
/// - Parameters:
91+
/// - repository: Name of the source repository.
92+
/// - reference: Tag or digest of the index to fetch.
93+
/// - Returns: The downloaded index.
94+
/// - Throws: If the download fails or the index cannot be decoded.
95+
public func getIndex(
96+
repository: ImageReference.Repository,
97+
reference: any ImageReference.Reference
98+
) async throws -> ImageIndex {
99+
fatalError()
100+
}
101+
102+
/// Get an image configuration record from the registry.
103+
/// - Parameters:
104+
/// - image: Reference to the image containing the record.
105+
/// - digest: Digest of the record.
106+
/// - Returns: The image confguration record stored in `repository` with digest `digest`.
107+
/// - Throws: If the blob cannot be decoded as an `ImageConfiguration`.
108+
///
109+
/// 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.
110+
public func getImageConfiguration(
111+
forImage image: ImageReference,
112+
digest: ImageReference.Digest
113+
) async throws -> ImageConfiguration {
114+
ImageConfiguration(
115+
architecture: architecture,
116+
os: os,
117+
rootfs: .init(_type: "layers", diff_ids: [])
118+
)
119+
}
120+
}

Sources/containertool/Extensions/RegistryClient+publish.swift

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Tar
2121
func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
2222
baseImage: ImageReference,
2323
destinationImage: ImageReference,
24-
source: Source?,
24+
source: Source,
2525
destination: Destination,
2626
architecture: String,
2727
os: String,
@@ -33,34 +33,17 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
3333

3434
// MARK: Find the base image
3535

36-
let baseImageManifest: ImageManifest
37-
let baseImageConfiguration: ImageConfiguration
38-
let baseImageDescriptor: ContentDescriptor
39-
if let source {
40-
(baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
41-
forImage: baseImage,
42-
architecture: architecture
43-
)
44-
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
36+
let (baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
37+
forImage: baseImage,
38+
architecture: architecture
39+
)
40+
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
4541

46-
baseImageConfiguration = try await source.getImageConfiguration(
47-
forImage: baseImage,
48-
digest: ImageReference.Digest(baseImageManifest.config.digest)
49-
)
50-
log("Found base image configuration: \(baseImageManifest.config.digest)")
51-
} else {
52-
baseImageManifest = .init(
53-
schemaVersion: 2,
54-
config: .init(mediaType: "scratch", digest: "scratch", size: 0),
55-
layers: []
56-
)
57-
baseImageConfiguration = .init(
58-
architecture: architecture,
59-
os: os,
60-
rootfs: .init(_type: "layers", diff_ids: [])
61-
)
62-
if verbose { log("Using scratch as base image") }
63-
}
42+
let baseImageConfiguration = try await source.getImageConfiguration(
43+
forImage: baseImage,
44+
digest: ImageReference.Digest(baseImageManifest.config.digest)
45+
)
46+
log("Found base image configuration: \(baseImageManifest.config.digest)")
6447

6548
// MARK: Upload resource layers
6649

@@ -142,15 +125,13 @@ func publishContainerImage<Source: ImageSource, Destination: ImageDestination>(
142125
// Copy the base image layers to the destination repository
143126
// Layers could be checked and uploaded concurrently
144127
// This could also happen in parallel with the application image build
145-
if let source {
146-
for layer in baseImageManifest.layers {
147-
try await source.copyBlob(
148-
digest: ImageReference.Digest(layer.digest),
149-
fromRepository: baseImage.repository,
150-
toClient: destination,
151-
toRepository: destinationImage.repository
152-
)
153-
}
128+
for layer in baseImageManifest.layers {
129+
try await source.copyBlob(
130+
digest: ImageReference.Digest(layer.digest),
131+
fromRepository: baseImage.repository,
132+
toClient: destination,
133+
toRepository: destinationImage.repository
134+
)
154135
}
155136

156137
// MARK: Upload application manifest

Sources/containertool/containertool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
190190

191191
// The base image may be stored on a different registry to the final destination, so two clients are needed.
192192
// `scratch` is a special case and requires no source client.
193-
let source: RegistryClient?
193+
let source: ImageSource
194194
if from == "scratch" {
195-
source = nil
195+
source = ScratchImage(architecture: architecture, os: os)
196196
} else {
197197
source = try await RegistryClient(
198198
registry: baseImage.registry,

0 commit comments

Comments
 (0)