Skip to content

Commit cf92449

Browse files
authored
ContainerRegistry: Separate registry operations into ImageSource and ImageDestination protocols (#148)
Motivation ---------- Currently, `RegistryClient` handles uploading and downloading container images to and from a registry. In fact, `containertool` creates two clients: one is used only to download images from the source repository, and the other is used only to upload images to the destination. Registries are read/write, but in the future we may need to handle sources of images which are read-only or write-only - for example when writing a container image out to an archive on disk. This change adds two new protocols, splitting up the operations needed to upload and download images. This allows for future clients which cannot handle both sets of operations. Modifications ------------- * Add new `ImageSource` and `ImageDestination` protocols, defining the functions needed to download and upload images, respectively. * Consolidate the `RegistryClient` extensions which implement the `ImageSource` and `ImageDestination` protocols. Result ------ Refactoring. No functional change, but responsibilities of different objects are clearer. Test Plan --------- Existing tests continue to pass.
1 parent 935912e commit cf92449

11 files changed

+515
-291
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
17+
/// A destination, such as a registry, to which container images can be uploaded.
18+
public protocol ImageDestination {
19+
/// Checks whether a blob exists.
20+
///
21+
/// - Parameters:
22+
/// - repository: Name of the destination repository.
23+
/// - digest: Digest of the requested blob.
24+
/// - Returns: True if the blob exists, otherwise false.
25+
/// - Throws: If the destination encounters an error.
26+
func blobExists(
27+
repository: ImageReference.Repository,
28+
digest: ImageReference.Digest
29+
) async throws -> Bool
30+
31+
/// Uploads a blob of unstructured data.
32+
///
33+
/// - Parameters:
34+
/// - repository: Name of the destination repository.
35+
/// - mediaType: mediaType field for returned ContentDescriptor.
36+
/// On the wire, all blob uploads are `application/octet-stream'.
37+
/// - data: Object to be uploaded.
38+
/// - Returns: An ContentDescriptor object representing the
39+
/// uploaded blob.
40+
/// - Throws: If the upload fails.
41+
func putBlob(
42+
repository: ImageReference.Repository,
43+
mediaType: String,
44+
data: Data
45+
) async throws -> ContentDescriptor
46+
47+
/// Encodes and uploads a JSON object.
48+
///
49+
/// - Parameters:
50+
/// - repository: Name of the destination repository.
51+
/// - mediaType: mediaType field for returned ContentDescriptor.
52+
/// On the wire, all blob uploads are `application/octet-stream'.
53+
/// - data: Object to be uploaded.
54+
/// - Returns: An ContentDescriptor object representing the
55+
/// uploaded blob.
56+
/// - Throws: If the blob cannot be encoded or the upload fails.
57+
///
58+
/// Some JSON objects, such as ImageConfiguration, are stored
59+
/// in the registry as plain blobs with MIME type "application/octet-stream".
60+
/// This function encodes the data parameter and uploads it as a generic blob.
61+
func putBlob<Body: Encodable>(
62+
repository: ImageReference.Repository,
63+
mediaType: String,
64+
data: Body
65+
) async throws -> ContentDescriptor
66+
67+
/// Encodes and uploads an image manifest.
68+
///
69+
/// - Parameters:
70+
/// - repository: Name of the destination repository.
71+
/// - reference: Optional tag to apply to this manifest.
72+
/// - manifest: Manifest to be uploaded.
73+
/// - Returns: An ContentDescriptor object representing the
74+
/// uploaded blob.
75+
/// - Throws: If the blob cannot be encoded or the upload fails.
76+
///
77+
/// Manifests are not treated as blobs by the distribution specification.
78+
/// They have their own MIME types and are uploaded to different
79+
/// registry endpoints than blobs.
80+
func putManifest(
81+
repository: ImageReference.Repository,
82+
reference: (any ImageReference.Reference)?,
83+
manifest: ImageManifest
84+
) async throws -> ContentDescriptor
85+
}
86+
87+
extension ImageDestination {
88+
/// Uploads a blob of unstructured data.
89+
///
90+
/// - Parameters:
91+
/// - repository: Name of the destination repository.
92+
/// - mediaType: mediaType field for returned ContentDescriptor.
93+
/// On the wire, all blob uploads are `application/octet-stream'.
94+
/// - data: Object to be uploaded.
95+
/// - Returns: An ContentDescriptor object representing the
96+
/// uploaded blob.
97+
/// - Throws: If the upload fails.
98+
public func putBlob(
99+
repository: ImageReference.Repository,
100+
mediaType: String = "application/octet-stream",
101+
data: Data
102+
) async throws -> ContentDescriptor {
103+
try await putBlob(repository: repository, mediaType: mediaType, data: data)
104+
}
105+
106+
/// Upload an image configuration record to the registry.
107+
/// - Parameters:
108+
/// - image: Reference to the image associated with the record.
109+
/// - configuration: An image configuration record
110+
/// - Returns: An `ContentDescriptor` referring to the blob stored in the registry.
111+
/// - Throws: If the blob upload fails.
112+
///
113+
/// Image configuration records are stored as blobs in the registry. This function encodes the provided configuration record and stores it as a blob in the registry.
114+
public func putImageConfiguration(
115+
forImage image: ImageReference,
116+
configuration: ImageConfiguration
117+
) async throws -> ContentDescriptor {
118+
try await putBlob(
119+
repository: image.repository,
120+
mediaType: "application/vnd.oci.image.config.v1+json",
121+
data: configuration
122+
)
123+
}
124+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
17+
/// A source, such as a registry, from which container images can be fetched.
18+
public protocol ImageSource {
19+
/// Fetches a blob of unstructured data.
20+
///
21+
/// - Parameters:
22+
/// - repository: Name of the source repository.
23+
/// - digest: Digest of the blob.
24+
/// - Returns: The downloaded data.
25+
/// - Throws: If the blob download fails.
26+
func getBlob(
27+
repository: ImageReference.Repository,
28+
digest: ImageReference.Digest
29+
) async throws -> Data
30+
31+
/// Fetches an image manifest.
32+
///
33+
/// - Parameters:
34+
/// - repository: Name of the source repository.
35+
/// - reference: Tag or digest of the manifest to fetch.
36+
/// - Returns: The downloaded manifest.
37+
/// - Throws: If the download fails or the manifest cannot be decoded.
38+
func getManifest(
39+
repository: ImageReference.Repository,
40+
reference: any ImageReference.Reference
41+
) async throws -> (ImageManifest, ContentDescriptor)
42+
43+
/// Fetches an image index.
44+
///
45+
/// - Parameters:
46+
/// - repository: Name of the source repository.
47+
/// - reference: Tag or digest of the index to fetch.
48+
/// - Returns: The downloaded index.
49+
/// - Throws: If the download fails or the index cannot be decoded.
50+
func getIndex(
51+
repository: ImageReference.Repository,
52+
reference: any ImageReference.Reference
53+
) async throws -> ImageIndex
54+
55+
/// Fetches an image configuration from the registry.
56+
///
57+
/// - Parameters:
58+
/// - image: Reference to the image containing the record.
59+
/// - digest: Digest of the configuration object to fetch.
60+
/// - Returns: The image confguration record.
61+
/// - Throws: If the download fails or the configuration record cannot be decoded.
62+
///
63+
/// Image configuration records are stored as blobs in the registry. This function retrieves
64+
/// the requested blob and tries to decode it as a configuration record.
65+
func getImageConfiguration(
66+
forImage image: ImageReference,
67+
digest: ImageReference.Digest
68+
) async throws -> ImageConfiguration
69+
}

Sources/ContainerRegistry/CheckAPI.swift renamed to Sources/ContainerRegistry/RegistryClient+CheckAPI.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
15+
import struct Foundation.URL
1616

17-
public extension RegistryClient {
17+
extension RegistryClient {
1818
/// Checks whether the registry supports v2 of the distribution specification.
1919
/// - Returns: an `true` if the registry supports the distribution specification.
2020
/// - Throws: if the registry does not support the distribution specification.
21-
static func checkAPI(client: HTTPClient, registryURL: URL) async throws -> AuthChallenge {
21+
public static func checkAPI(client: HTTPClient, registryURL: URL) async throws -> AuthChallenge {
2222
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
2323

2424
// The registry indicates that it supports the v2 protocol by returning a 200 OK response.

Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift

Lines changed: 0 additions & 50 deletions
This file was deleted.

Sources/ContainerRegistry/Blobs.swift renamed to Sources/ContainerRegistry/RegistryClient+ImageDestination.swift

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
15+
import struct Foundation.Data
16+
import struct Foundation.URL
1617
import HTTPTypes
1718

18-
extension RegistryClient {
19+
extension RegistryClient: ImageDestination {
1920
// Internal helper method to initiate a blob upload in 'two shot' mode
2021
func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL {
2122
// Upload in "two shot" mode.
@@ -48,10 +49,18 @@ extension RegistryClient {
4849

4950
return locationURL
5051
}
51-
}
5252

53-
public extension RegistryClient {
54-
func blobExists(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Bool {
53+
/// Checks whether a blob exists.
54+
///
55+
/// - Parameters:
56+
/// - repository: Name of the destination repository.
57+
/// - digest: Digest of the requested blob.
58+
/// - Returns: True if the blob exists, otherwise false.
59+
/// - Throws: If the destination encounters an error.
60+
public func blobExists(
61+
repository: ImageReference.Repository,
62+
digest: ImageReference.Digest
63+
) async throws -> Bool {
5564
do {
5665
let _ = try await executeRequestThrowing(
5766
.head(repository, path: "blobs/\(digest)"),
@@ -61,21 +70,6 @@ public extension RegistryClient {
6170
} catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false }
6271
}
6372

64-
/// Fetches an unstructured blob of data from the registry.
65-
///
66-
/// - Parameters:
67-
/// - repository: Name of the repository containing the blob.
68-
/// - digest: Digest of the blob.
69-
/// - Returns: The downloaded data.
70-
/// - Throws: If the blob download fails.
71-
func getBlob(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Data {
72-
try await executeRequestThrowing(
73-
.get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]),
74-
decodingErrors: [.notFound]
75-
)
76-
.data
77-
}
78-
7973
/// Uploads a blob to the registry.
8074
///
8175
/// This function uploads a blob of unstructured data to the registry.
@@ -87,10 +81,11 @@ public extension RegistryClient {
8781
/// - Returns: An ContentDescriptor object representing the
8882
/// uploaded blob.
8983
/// - Throws: If the blob cannot be encoded or the upload fails.
90-
func putBlob(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Data)
91-
async throws
92-
-> ContentDescriptor
93-
{
84+
public func putBlob(
85+
repository: ImageReference.Repository,
86+
mediaType: String = "application/octet-stream",
87+
data: Data
88+
) async throws -> ContentDescriptor {
9489
// Ask the server to open a session and tell us where to upload our data
9590
let location = try await startBlobUploadSession(repository: repository)
9691

@@ -133,14 +128,53 @@ public extension RegistryClient {
133128
/// Some JSON objects, such as ImageConfiguration, are stored
134129
/// in the registry as plain blobs with MIME type "application/octet-stream".
135130
/// This function encodes the data parameter and uploads it as a generic blob.
136-
func putBlob<Body: Encodable>(
131+
public func putBlob<Body: Encodable>(
137132
repository: ImageReference.Repository,
138133
mediaType: String = "application/octet-stream",
139134
data: Body
140-
)
141-
async throws -> ContentDescriptor
142-
{
135+
) async throws -> ContentDescriptor {
143136
let encoded = try encoder.encode(data)
144137
return try await putBlob(repository: repository, mediaType: mediaType, data: encoded)
145138
}
139+
140+
/// Encodes and uploads an image manifest.
141+
///
142+
/// - Parameters:
143+
/// - repository: Name of the destination repository.
144+
/// - reference: Optional tag to apply to this manifest.
145+
/// - manifest: Manifest to be uploaded.
146+
/// - Returns: An ContentDescriptor object representing the
147+
/// uploaded blob.
148+
/// - Throws: If the blob cannot be encoded or the upload fails.
149+
///
150+
/// Manifests are not treated as blobs by the distribution specification.
151+
/// They have their own MIME types and are uploaded to different
152+
public func putManifest(
153+
repository: ImageReference.Repository,
154+
reference: (any ImageReference.Reference)? = nil,
155+
manifest: ImageManifest
156+
) async throws -> ContentDescriptor {
157+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
158+
159+
let encoded = try encoder.encode(manifest)
160+
let digest = ImageReference.Digest(of: encoded)
161+
let mediaType = manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json"
162+
163+
let _ = try await executeRequestThrowing(
164+
.put(
165+
repository,
166+
path: "manifests/\(reference ?? digest)",
167+
contentType: mediaType
168+
),
169+
uploading: encoded,
170+
expectingStatus: .created,
171+
decodingErrors: [.notFound]
172+
)
173+
174+
return ContentDescriptor(
175+
mediaType: mediaType,
176+
digest: "\(digest)",
177+
size: Int64(encoded.count)
178+
)
179+
}
146180
}

0 commit comments

Comments
 (0)