Skip to content

Commit c8ef624

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

File tree

10 files changed

+138
-74
lines changed

10 files changed

+138
-74
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: 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: 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: 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: 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: 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: 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: 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: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
import RegexBuilder
1616

17-
enum ReferenceError: Error { case unexpected(String) }
17+
public enum ReferenceError: Error, Equatable {
18+
case emptyString
19+
case unexpected(String)
20+
}
1821

1922
// https://github.com/distribution/distribution/blob/v2.7.1/reference/reference.go
2023
// Split the image reference into a registry and a name part.
@@ -47,12 +50,36 @@ func splitName(_ name: String) throws -> (String, String) {
4750
return (String(tagSplit[0]), String(tagSplit[1]))
4851
}
4952

53+
/// Repository refers a repository (image namespace) on a container registry
54+
public struct Repository: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible {
55+
var value: String
56+
57+
public init(_ rawValue: String) throws {
58+
// Reference handling in github.com/distribution reports empty and uppercase as specific errors.
59+
// All other errors caused are reported as generic format errors.
60+
guard rawValue.count > 0 else {
61+
throw ReferenceError.emptyString
62+
}
63+
64+
value = rawValue
65+
}
66+
67+
public var description: String {
68+
value
69+
}
70+
71+
/// Printable description of an ImageReference in a form suitable for debugging.
72+
public var debugDescription: String {
73+
"Repository(\(value))"
74+
}
75+
}
76+
5077
/// ImageReference points to an image stored on a container registry
5178
public struct ImageReference: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible {
5279
/// The registry which contains this image
5380
public var registry: String
5481
/// The repository which contains this image
55-
public var repository: String
82+
public var repository: Repository
5683
/// The tag identifying the image.
5784
public var reference: String
5885

@@ -72,19 +99,20 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust
7299
// moby/moby assumes that these names refer to images in `library`: `library/swift` or `library/swift:slim`.
73100
// This special case only applies when using Docker Hub, so `example.com/swift` is not expanded `example.com/library/swift`
74101
if self.registry == "index.docker.io" && !repository.contains("/") {
75-
self.repository = "library/\(repository)"
102+
self.repository = try Repository("library/\(repository)")
76103
} else {
77-
self.repository = repository
104+
self.repository = try Repository(repository)
78105
}
79106
self.reference = reference
80107
}
81108

82109
/// Creates an ImageReference from separate registry, repository and reference strings.
110+
/// Used only in tests.
83111
/// - Parameters:
84112
/// - registry: The registry which stores the image data.
85113
/// - repository: The repository within the registry which holds the image.
86114
/// - reference: The tag identifying the image.
87-
public init(registry: String, repository: String, reference: String) {
115+
init(registry: String, repository: Repository, reference: String) {
88116
self.registry = registry
89117
self.repository = repository
90118
self.reference = reference

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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: Repository,
2828
toClient destClient: RegistryClient,
29-
toRepository destRepository: String
29+
toRepository destRepository: 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: Repository,
4949
contents: [UInt8],
5050
mediaType: String = "application/vnd.oci.image.layer.v1.tar+gzip"
5151
) async throws -> ImageLayer {

0 commit comments

Comments
 (0)