Skip to content

Commit c793434

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

File tree

3 files changed

+157
-39
lines changed

3 files changed

+157
-39
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.JSONEncoder
17+
18+
/// ScratchImage is a special-purpose ImageSource which represents the scratch image.
19+
public struct ScratchImage {
20+
var encoder: JSONEncoder
21+
22+
var architecture: String
23+
var os: String
24+
25+
var configuration: ImageConfiguration
26+
var manifest: ImageManifest
27+
var manifestDescriptor: ContentDescriptor
28+
var index: ImageIndex
29+
30+
public init(architecture: String, os: String) {
31+
self.encoder = JSONEncoder()
32+
self.encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
33+
self.encoder.dateEncodingStrategy = .iso8601
34+
35+
self.architecture = architecture
36+
self.os = os
37+
38+
self.configuration = ImageConfiguration(
39+
architecture: architecture,
40+
os: os,
41+
rootfs: .init(_type: "layers", diff_ids: [])
42+
)
43+
let encodedConfiguration = try! encoder.encode(self.configuration)
44+
45+
self.manifest = ImageManifest(
46+
schemaVersion: 2,
47+
config: ContentDescriptor(
48+
mediaType: "application/vnd.oci.image.config.v1+json",
49+
digest: "\(ImageReference.Digest(of: encodedConfiguration))",
50+
size: Int64(encodedConfiguration.count)
51+
),
52+
layers: []
53+
)
54+
let encodedManifest = try! encoder.encode(self.manifest)
55+
56+
self.manifestDescriptor = ContentDescriptor(
57+
mediaType: "application/vnd.oci.image.manifest.v1+json",
58+
digest: "\(ImageReference.Digest(of: encodedManifest))",
59+
size: Int64(encodedManifest.count)
60+
)
61+
62+
self.index = ImageIndex(
63+
schemaVersion: 2,
64+
mediaType: "application/vnd.oci.image.index.v1+json",
65+
manifests: [
66+
ContentDescriptor(
67+
mediaType: "application/vnd.oci.image.manifest.v1+json",
68+
digest: "\(ImageReference.Digest(of: encodedManifest))",
69+
size: Int64(encodedManifest.count),
70+
platform: .init(architecture: architecture, os: os),
71+
)
72+
]
73+
)
74+
}
75+
}
76+
77+
extension ScratchImage: ImageSource {
78+
/// The scratch image has no data layers, so `getBlob` returns an empty data blob.
79+
///
80+
/// - Parameters:
81+
/// - repository: Name of the repository containing the blob.
82+
/// - digest: Digest of the blob.
83+
/// - Returns: An empty blob.
84+
/// - Throws: Does not throw, but signature must match the `ImageSource` protocol requirements.
85+
public func getBlob(
86+
repository: ImageReference.Repository,
87+
digest: ImageReference.Digest
88+
) async throws -> Data {
89+
Data()
90+
}
91+
92+
/// Returns an empty manifest for the scratch image, with no image layers.
93+
///
94+
/// - Parameters:
95+
/// - repository: Name of the source repository.
96+
/// - reference: Tag or digest of the manifest to fetch.
97+
/// - Returns: The downloaded manifest.
98+
/// - Throws: Does not throw, but signature must match the `ImageSource` protocol requirements.
99+
public func getManifest(
100+
repository: ImageReference.Repository,
101+
reference: any ImageReference.Reference
102+
) async throws -> (ImageManifest, ContentDescriptor) {
103+
(self.manifest, self.manifestDescriptor)
104+
}
105+
106+
/// Fetches an image index.
107+
///
108+
/// - Parameters:
109+
/// - repository: Name of the source repository.
110+
/// - reference: Tag or digest of the index to fetch.
111+
/// - Returns: The downloaded index.
112+
/// - Throws: Does not throw, but signature must match the `ImageSource` protocol requirements.
113+
public func getIndex(
114+
repository: ImageReference.Repository,
115+
reference: any ImageReference.Reference
116+
) async throws -> ImageIndex {
117+
self.index
118+
}
119+
120+
/// Returns an almost empty image configuration scratch image.
121+
/// The processor architecture and operating system fields are populated,
122+
/// but the layer list is empty.
123+
///
124+
/// - Parameters:
125+
/// - image: Reference to the image containing the record.
126+
/// - digest: Digest of the record.
127+
/// - Returns: A suitable configuration for the scratch image.
128+
/// - Throws: Does not throw, but signature must match the `ImageSource` protocol requirements.
129+
///
130+
/// 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.
131+
public func getImageConfiguration(
132+
forImage image: ImageReference,
133+
digest: ImageReference.Digest
134+
) async throws -> ImageConfiguration {
135+
self.configuration
136+
}
137+
}

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)