Skip to content

Commit a894e29

Browse files
committed
ContainerRegistry: Introduce Repository type
1 parent 3030089 commit a894e29

File tree

10 files changed

+141
-77
lines changed

10 files changed

+141
-77
lines changed

Sources/ContainerRegistry/AuthHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public struct AuthHandler {
158158

159159
public func auth(
160160
registry: URL,
161-
repository: String,
161+
repository: ImageReference.Repository,
162162
actions: [String],
163163
withScheme scheme: AuthChallenge,
164164
usingClient client: HTTPClient

Sources/ContainerRegistry/Blobs.swift

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ public func digest<D: DataProtocol>(of data: D) -> String {
2828

2929
extension RegistryClient {
3030
// Internal helper method to initiate a blob upload in 'two shot' mode
31-
func startBlobUploadSession(repository: String) async throws -> URL {
32-
precondition(repository.count > 0, "repository must not be an empty string")
33-
31+
func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL {
3432
// Upload in "two shot" mode.
3533
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put
3634
// - POST to obtain a session ID.
@@ -67,8 +65,7 @@ extension RegistryClient {
6765
extension HTTPField.Name { static let dockerContentDigest = Self("Docker-Content-Digest")! }
6866

6967
public extension RegistryClient {
70-
func blobExists(repository: String, digest: String) async throws -> Bool {
71-
precondition(repository.count > 0, "repository must not be an empty string")
68+
func blobExists(repository: ImageReference.Repository, digest: String) async throws -> Bool {
7269
precondition(digest.count > 0)
7370

7471
do {
@@ -87,8 +84,7 @@ public extension RegistryClient {
8784
/// - digest: Digest of the blob.
8885
/// - Returns: The downloaded data.
8986
/// - Throws: If the blob download fails.
90-
func getBlob(repository: String, digest: String) async throws -> Data {
91-
precondition(repository.count > 0, "repository must not be an empty string")
87+
func getBlob(repository: ImageReference.Repository, digest: String) async throws -> Data {
9288
precondition(digest.count > 0, "digest must not be an empty string")
9389

9490
return try await executeRequestThrowing(
@@ -110,8 +106,7 @@ public extension RegistryClient {
110106
/// in the registry as plain blobs with MIME type "application/octet-stream".
111107
/// This function attempts to decode the received data without reference
112108
/// to the MIME type.
113-
func getBlob<Response: Decodable>(repository: String, digest: String) async throws -> Response {
114-
precondition(repository.count > 0, "repository must not be an empty string")
109+
func getBlob<Response: Decodable>(repository: ImageReference.Repository, digest: String) async throws -> Response {
115110
precondition(digest.count > 0, "digest must not be an empty string")
116111

117112
return try await executeRequestThrowing(
@@ -132,11 +127,9 @@ public extension RegistryClient {
132127
/// - Returns: An ContentDescriptor object representing the
133128
/// uploaded blob.
134129
/// - Throws: If the blob cannot be encoded or the upload fails.
135-
func putBlob(repository: String, mediaType: String = "application/octet-stream", data: Data) async throws
130+
func putBlob(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Data) async throws
136131
-> ContentDescriptor
137132
{
138-
precondition(repository.count > 0, "repository must not be an empty string")
139-
140133
// Ask the server to open a session and tell us where to upload our data
141134
let location = try await startBlobUploadSession(repository: repository)
142135

@@ -179,7 +172,7 @@ public extension RegistryClient {
179172
/// Some JSON objects, such as ImageConfiguration, are stored
180173
/// in the registry as plain blobs with MIME type "application/octet-stream".
181174
/// This function encodes the data parameter and uploads it as a generic blob.
182-
func putBlob<Body: Encodable>(repository: String, mediaType: String = "application/octet-stream", data: Body)
175+
func putBlob<Body: Encodable>(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Body)
183176
async throws -> ContentDescriptor
184177
{
185178
let encoded = try encoder.encode(data)

Sources/ContainerRegistry/ImageReference.swift

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414

1515
import RegexBuilder
1616

17-
enum ReferenceError: Error { case unexpected(String) }
18-
1917
// https://github.com/distribution/distribution/blob/v2.7.1/reference/reference.go
2018
// Split the image reference into a registry and a name part.
2119
func splitReference(_ reference: String) throws -> (String?, String) {
2220
let splits = reference.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false)
23-
if splits.count == 0 { throw ReferenceError.unexpected("unexpected error") }
21+
if splits.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") }
2422

2523
if splits.count == 1 { return (nil, reference) }
2624

@@ -39,23 +37,29 @@ func splitName(_ name: String) throws -> (String, String) {
3937
if digestSplit.count == 2 { return (String(digestSplit[0]), String(digestSplit[1])) }
4038

4139
let tagSplit = name.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
42-
if tagSplit.count == 0 { throw ReferenceError.unexpected("unexpected error") }
40+
if tagSplit.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") }
4341

4442
if tagSplit.count == 1 { return (name, "latest") }
4543

4644
// assert splits == 2
4745
return (String(tagSplit[0]), String(tagSplit[1]))
4846
}
4947

48+
/// Repository refers a repository (image namespace) on a container registry
49+
5050
/// ImageReference points to an image stored on a container registry
5151
public struct ImageReference: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible {
5252
/// The registry which contains this image
5353
public var registry: String
5454
/// The repository which contains this image
55-
public var repository: String
55+
public var repository: Repository
5656
/// The tag identifying the image.
5757
public var reference: String
5858

59+
public enum ValidationError: Error {
60+
case unexpected(String)
61+
}
62+
5963
/// Creates an ImageReference from an image reference string.
6064
/// - Parameters:
6165
/// - reference: The reference to parse.
@@ -72,19 +76,20 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust
7276
// moby/moby assumes that these names refer to images in `library`: `library/swift` or `library/swift:slim`.
7377
// This special case only applies when using Docker Hub, so `example.com/swift` is not expanded `example.com/library/swift`
7478
if self.registry == "index.docker.io" && !repository.contains("/") {
75-
self.repository = "library/\(repository)"
79+
self.repository = try Repository("library/\(repository)")
7680
} else {
77-
self.repository = repository
81+
self.repository = try Repository(repository)
7882
}
7983
self.reference = reference
8084
}
8185

8286
/// Creates an ImageReference from separate registry, repository and reference strings.
87+
/// Used only in tests.
8388
/// - Parameters:
8489
/// - registry: The registry which stores the image data.
8590
/// - repository: The repository within the registry which holds the image.
8691
/// - reference: The tag identifying the image.
87-
public init(registry: String, repository: String, reference: String) {
92+
init(registry: String, repository: Repository, reference: String) {
8893
self.registry = registry
8994
self.repository = repository
9095
self.reference = reference
@@ -104,3 +109,32 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust
104109
"ImageReference(registry: \(registry), repository: \(repository), reference: \(reference))"
105110
}
106111
}
112+
113+
extension ImageReference {
114+
public struct Repository: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible {
115+
var value: String
116+
117+
public enum ValidationError: Error {
118+
case emptyString
119+
}
120+
121+
public init(_ rawValue: String) throws {
122+
// Reference handling in github.com/distribution reports empty and uppercase as specific errors.
123+
// All other errors caused are reported as generic format errors.
124+
guard rawValue.count > 0 else {
125+
throw ValidationError.emptyString
126+
}
127+
128+
value = rawValue
129+
}
130+
131+
public var description: String {
132+
value
133+
}
134+
135+
/// Printable description of an ImageReference in a form suitable for debugging.
136+
public var debugDescription: String {
137+
"Repository(\(value))"
138+
}
139+
}
140+
}

Sources/ContainerRegistry/Manifests.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
public extension RegistryClient {
16-
func putManifest(repository: String, reference: String, manifest: ImageManifest) async throws -> String {
16+
func putManifest(repository: ImageReference.Repository, reference: String, manifest: ImageManifest) async throws -> String {
1717
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
18-
precondition(repository.count > 0, "repository must not be an empty string")
19-
precondition(reference.count > 0, "reference must not be an empty string")
18+
precondition("\(reference)".count > 0, "reference must not be an empty string")
2019

2120
let httpResponse = try await executeRequestThrowing(
2221
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
@@ -41,9 +40,8 @@ public extension RegistryClient {
4140
.absoluteString
4241
}
4342

44-
func getManifest(repository: String, reference: String) async throws -> ImageManifest {
43+
func getManifest(repository: ImageReference.Repository, reference: String) async throws -> ImageManifest {
4544
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
46-
precondition(repository.count > 0, "repository must not be an empty string")
4745
precondition(reference.count > 0, "reference must not be an empty string")
4846

4947
return try await executeRequestThrowing(
@@ -60,8 +58,7 @@ public extension RegistryClient {
6058
.data
6159
}
6260

63-
func getIndex(repository: String, reference: String) async throws -> ImageIndex {
64-
precondition(repository.count > 0, "repository must not be an empty string")
61+
func getIndex(repository: ImageReference.Repository, reference: String) async throws -> ImageIndex {
6562
precondition(reference.count > 0, "reference must not be an empty string")
6663

6764
return try await executeRequestThrowing(

Sources/ContainerRegistry/RegistryClient.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ extension URL {
127127
/// - repository: The name of the repository. May include path separators.
128128
/// - endpoint: The distribution endpoint e.g. "tags/list"
129129
/// - Returns: A fully-qualified URL for the endpoint.
130-
func distributionEndpoint(forRepository repository: String, andEndpoint endpoint: String) -> URL {
130+
func distributionEndpoint(forRepository repository: ImageReference.Repository, andEndpoint endpoint: String) -> URL {
131131
self.appendingPathComponent("/v2/\(repository)/\(endpoint)")
132132
}
133133
}
@@ -141,7 +141,7 @@ extension RegistryClient {
141141
}
142142

143143
var method: HTTPRequest.Method // HTTP method
144-
var repository: String // Repository path on the registry
144+
var repository: ImageReference.Repository // Repository path on the registry
145145
var destination: Destination // Destination of the operation: can be a subpath or remote URL
146146
var actions: [String] // Actions required by this operation
147147
var accepting: [String] = [] // Acceptable response types
@@ -156,7 +156,7 @@ extension RegistryClient {
156156

157157
// Convenience constructors
158158
static func get(
159-
_ repository: String,
159+
_ repository: ImageReference.Repository,
160160
path: String,
161161
actions: [String]? = nil,
162162
accepting: [String] = [],
@@ -173,7 +173,7 @@ extension RegistryClient {
173173
}
174174

175175
static func get(
176-
_ repository: String,
176+
_ repository: ImageReference.Repository,
177177
url: URL,
178178
actions: [String]? = nil,
179179
accepting: [String] = [],
@@ -190,7 +190,7 @@ extension RegistryClient {
190190
}
191191

192192
static func head(
193-
_ repository: String,
193+
_ repository: ImageReference.Repository,
194194
path: String,
195195
actions: [String]? = nil,
196196
accepting: [String] = [],
@@ -208,7 +208,7 @@ extension RegistryClient {
208208

209209
/// This handles the 'put' case where the registry gives us a location URL which we must not alter, aside from adding the digest to it
210210
static func put(
211-
_ repository: String,
211+
_ repository: ImageReference.Repository,
212212
url: URL,
213213
actions: [String]? = nil,
214214
accepting: [String] = [],
@@ -225,7 +225,7 @@ extension RegistryClient {
225225
}
226226

227227
static func put(
228-
_ repository: String,
228+
_ repository: ImageReference.Repository,
229229
path: String,
230230
actions: [String]? = nil,
231231
accepting: [String] = [],
@@ -242,7 +242,7 @@ extension RegistryClient {
242242
}
243243

244244
static func post(
245-
_ repository: String,
245+
_ repository: ImageReference.Repository,
246246
path: String,
247247
actions: [String]? = nil,
248248
accepting: [String] = [],

Sources/ContainerRegistry/Tags.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
public extension RegistryClient {
16-
func getTags(repository: String) async throws -> Tags {
16+
func getTags(repository: ImageReference.Repository) async throws -> Tags {
1717
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-tags
18-
precondition(repository.count > 0, "repository must not be an empty string")
19-
20-
return try await executeRequestThrowing(.get(repository, path: "tags/list"), decodingErrors: [.notFound]).data
18+
try await executeRequestThrowing(.get(repository, path: "tags/list"), decodingErrors: [.notFound]).data
2119
}
2220
}

Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ extension RegistryClient {
2424
/// - Throws: If the copy cannot be completed.
2525
func copyBlob(
2626
digest: String,
27-
fromRepository sourceRepository: String,
27+
fromRepository sourceRepository: ImageReference.Repository,
2828
toClient destClient: RegistryClient,
29-
toRepository destRepository: String
29+
toRepository destRepository: ImageReference.Repository
3030
) async throws {
3131
if try await destClient.blobExists(repository: destRepository, digest: digest) {
3232
log("Layer \(digest): already exists")

Sources/containertool/Extensions/RegistryClient+Layers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension RegistryClient {
4545
// A layer is a tarball, optionally compressed using gzip or zstd
4646
// See https://github.com/opencontainers/image-spec/blob/main/media-types.md
4747
func uploadLayer(
48-
repository: String,
48+
repository: ImageReference.Repository,
4949
contents: [UInt8],
5050
mediaType: String = "application/vnd.oci.image.layer.v1.tar+gzip"
5151
) async throws -> ImageLayer {

0 commit comments

Comments
 (0)