Skip to content

Commit 8b5ffcc

Browse files
committed
ContainerRegistry: Define ImageDestination protocol
1 parent 41092be commit 8b5ffcc

File tree

6 files changed

+118
-12
lines changed

6 files changed

+118
-12
lines changed
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+
17+
/// A destination, such as a registry, to which container images can be uploaded.
18+
public protocol ImageDestination {
19+
/// Uploads a blob of unstructured data.
20+
///
21+
/// - Parameters:
22+
/// - repository: Name of the destination repository.
23+
/// - mediaType: mediaType field for returned ContentDescriptor.
24+
/// On the wire, all blob uploads are `application/octet-stream'.
25+
/// - data: Object to be uploaded.
26+
/// - Returns: An ContentDescriptor object representing the
27+
/// uploaded blob.
28+
/// - Throws: If the upload fails.
29+
func putBlob(
30+
repository: ImageReference.Repository,
31+
mediaType: String,
32+
data: Data
33+
) async throws -> ContentDescriptor
34+
35+
/// Encodes and uploads a JSON object.
36+
///
37+
/// - Parameters:
38+
/// - repository: Name of the destination repository.
39+
/// - mediaType: mediaType field for returned ContentDescriptor.
40+
/// On the wire, all blob uploads are `application/octet-stream'.
41+
/// - data: Object to be uploaded.
42+
/// - Returns: An ContentDescriptor object representing the
43+
/// uploaded blob.
44+
/// - Throws: If the blob cannot be encoded or the upload fails.
45+
///
46+
/// Some JSON objects, such as ImageConfiguration, are stored
47+
/// in the registry as plain blobs with MIME type "application/octet-stream".
48+
/// This function encodes the data parameter and uploads it as a generic blob.
49+
func putBlob<Body: Encodable>(
50+
repository: ImageReference.Repository,
51+
mediaType: String,
52+
data: Body
53+
) async throws -> ContentDescriptor
54+
55+
/// Checks whether a blob exists.
56+
///
57+
/// - Parameters:
58+
/// - repository: Name of the destination repository.
59+
/// - digest: Digest of the requested blob.
60+
/// - Returns: True if the blob exists, otherwise false.
61+
/// - Throws: If the destination encounters an error.
62+
func blobExists(
63+
repository: ImageReference.Repository,
64+
digest: ImageReference.Digest
65+
) async throws -> Bool
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+
}

Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ extension RegistryClient {
2828
let data = try await getBlob(repository: image.repository, digest: digest)
2929
return try decoder.decode(ImageConfiguration.self, from: data)
3030
}
31+
}
3132

33+
extension ImageDestination {
3234
/// Upload an image configuration record to the registry.
3335
/// - Parameters:
3436
/// - image: Reference to the image associated with the record.

Sources/ContainerRegistry/RegistryClient.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,6 @@ extension RegistryClient {
374374

375375
/// Make decoded registry errors throwable
376376
extension DistributionErrors: Error {}
377+
378+
/// Images can be uploaded to a registry using a RegistryClient instance
379+
extension RegistryClient: ImageDestination {}

Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension RegistryClient {
2525
func copyBlob(
2626
digest: ImageReference.Digest,
2727
fromRepository sourceRepository: ImageReference.Repository,
28-
toClient destClient: RegistryClient,
28+
toClient destClient: ImageDestination,
2929
toRepository destRepository: ImageReference.Repository
3030
) async throws {
3131
if try await destClient.blobExists(repository: destRepository, digest: digest) {

Sources/containertool/Extensions/RegistryClient+Layers.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,21 @@ extension RegistryClient {
3737
)
3838
}
3939
}
40+
}
4041

41-
typealias DiffID = ImageReference.Digest
42-
struct ImageLayer {
43-
var descriptor: ContentDescriptor
44-
var diffID: DiffID
45-
}
46-
42+
extension ImageDestination {
4743
// A layer is a tarball, optionally compressed using gzip or zstd
4844
// See https://github.com/opencontainers/image-spec/blob/main/media-types.md
4945
func uploadLayer(
5046
repository: ImageReference.Repository,
5147
contents: [UInt8],
5248
mediaType: String = "application/vnd.oci.image.layer.v1.tar+gzip"
53-
) async throws -> ImageLayer {
49+
) async throws -> (descriptor: ContentDescriptor, diffID: ImageReference.Digest) {
5450
// The diffID is the hash of the unzipped layer tarball
5551
let diffID = ImageReference.Digest(of: contents)
5652
// The layer blob is the gzipped tarball; the descriptor is the hash of this gzipped blob
5753
let blob = Data(gzip(contents))
5854
let descriptor = try await putBlob(repository: repository, mediaType: mediaType, data: blob)
59-
return ImageLayer(descriptor: descriptor, diffID: diffID)
55+
return (descriptor: descriptor, diffID: diffID)
6056
}
6157
}

Sources/containertool/Extensions/RegistryClient+publish.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import struct Foundation.URL
1818
import ContainerRegistry
1919
import Tar
2020

21-
func publishContainerImage(
21+
func publishContainerImage<Destination: ImageDestination>(
2222
baseImage: ImageReference,
2323
destinationImage: ImageReference,
2424
source: RegistryClient?,
25-
destination: RegistryClient,
25+
destination: Destination,
2626
architecture: String,
2727
os: String,
2828
resources: [String],
@@ -64,7 +64,7 @@ func publishContainerImage(
6464

6565
// MARK: Upload resource layers
6666

67-
var resourceLayers: [RegistryClient.ImageLayer] = []
67+
var resourceLayers: [(descriptor: ContentDescriptor, diffID: ImageReference.Digest)] = []
6868
for resourceDir in resources {
6969
let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes
7070
let resourceLayer = try await destination.uploadLayer(

0 commit comments

Comments
 (0)