Skip to content

Commit c60a66a

Browse files
committed
ContainerRegistry: Define ImageDestination protocol
1 parent 5518c22 commit c60a66a

File tree

8 files changed

+300
-264
lines changed

8 files changed

+300
-264
lines changed

Sources/ContainerRegistry/Blobs.swift

Lines changed: 0 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,7 @@
1515
import Foundation
1616
import HTTPTypes
1717

18-
extension RegistryClient {
19-
// Internal helper method to initiate a blob upload in 'two shot' mode
20-
func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL {
21-
// Upload in "two shot" mode.
22-
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put
23-
// - POST to obtain a session ID.
24-
// - Do not include the digest.
25-
// Response will include a 'Location' header telling us where to PUT the blob data.
26-
let httpResponse = try await executeRequestThrowing(
27-
.post(repository, path: "blobs/uploads/"),
28-
expectingStatus: .accepted, // expected response code for a "two-shot" upload
29-
decodingErrors: [.notFound]
30-
)
31-
32-
guard let location = httpResponse.response.headerFields[.location] else {
33-
throw HTTPClientError.missingResponseHeader("Location")
34-
}
35-
36-
guard let locationURL = URL(string: location) else {
37-
throw RegistryClientError.invalidUploadLocation("\(location)")
38-
}
39-
40-
// The location may be either an absolute URL or a relative URL
41-
// If it is relative we need to make it absolute
42-
guard locationURL.host != nil else {
43-
guard let absoluteURL = URL(string: location, relativeTo: registryURL) else {
44-
throw RegistryClientError.invalidUploadLocation("\(location)")
45-
}
46-
return absoluteURL
47-
}
48-
49-
return locationURL
50-
}
51-
}
52-
5318
public extension RegistryClient {
54-
func blobExists(repository: ImageReference.Repository, digest: ImageReference.Digest) async throws -> Bool {
55-
do {
56-
let _ = try await executeRequestThrowing(
57-
.head(repository, path: "blobs/\(digest)"),
58-
decodingErrors: [.notFound]
59-
)
60-
return true
61-
} catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false }
62-
}
63-
6419
/// Fetches an unstructured blob of data from the registry.
6520
///
6621
/// - Parameters:
@@ -75,72 +30,4 @@ public extension RegistryClient {
7530
)
7631
.data
7732
}
78-
79-
/// Uploads a blob to the registry.
80-
///
81-
/// This function uploads a blob of unstructured data to the registry.
82-
/// - Parameters:
83-
/// - repository: Name of the destination repository.
84-
/// - mediaType: mediaType field for returned ContentDescriptor.
85-
/// On the wire, all blob uploads are `application/octet-stream'.
86-
/// - data: Object to be uploaded.
87-
/// - Returns: An ContentDescriptor object representing the
88-
/// uploaded blob.
89-
/// - 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-
{
94-
// Ask the server to open a session and tell us where to upload our data
95-
let location = try await startBlobUploadSession(repository: repository)
96-
97-
// Append the digest to the upload location, as the specification requires.
98-
// The server's URL is arbitrary and might already contain query items which we must not overwrite.
99-
// The URL could even point to a different host.
100-
let digest = ImageReference.Digest(of: data)
101-
let uploadURL = location.appending(queryItems: [.init(name: "digest", value: "\(digest)")])
102-
103-
let httpResponse = try await executeRequestThrowing(
104-
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
105-
.put(repository, url: uploadURL, contentType: "application/octet-stream"),
106-
uploading: data,
107-
expectingStatus: .created,
108-
decodingErrors: [.badRequest, .notFound]
109-
)
110-
111-
// The registry could compute a different digest and we should use its value
112-
// as the canonical digest for linking blobs. If the registry sends a digest we
113-
// should check that it matches our locally-calculated digest.
114-
if let serverDigest = httpResponse.response.headerFields[.dockerContentDigest] {
115-
assert("\(digest)" == serverDigest)
116-
}
117-
return .init(mediaType: mediaType, digest: "\(digest)", size: Int64(data.count))
118-
}
119-
120-
/// Uploads a blob to the registry.
121-
///
122-
/// This function converts an encodable blob to an `application/octet-stream',
123-
/// calculates its digest and uploads it to the registry.
124-
/// - Parameters:
125-
/// - repository: Name of the destination repository.
126-
/// - mediaType: mediaType field for returned ContentDescriptor.
127-
/// On the wire, all blob uploads are `application/octet-stream'.
128-
/// - data: Object to be uploaded.
129-
/// - Returns: An ContentDescriptor object representing the
130-
/// uploaded blob.
131-
/// - Throws: If the blob cannot be encoded or the upload fails.
132-
///
133-
/// Some JSON objects, such as ImageConfiguration, are stored
134-
/// in the registry as plain blobs with MIME type "application/octet-stream".
135-
/// This function encodes the data parameter and uploads it as a generic blob.
136-
func putBlob<Body: Encodable>(
137-
repository: ImageReference.Repository,
138-
mediaType: String = "application/octet-stream",
139-
data: Body
140-
)
141-
async throws -> ContentDescriptor
142-
{
143-
let encoded = try encoder.encode(data)
144-
return try await putBlob(repository: repository, mediaType: mediaType, data: encoded)
145-
}
14633
}
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+
}

Sources/ContainerRegistry/Manifests.swift

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

Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift

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

0 commit comments

Comments
 (0)