@@ -115,33 +115,165 @@ public struct RegistryClient {
115
115
let urlsession = URLSession ( configuration: . ephemeral)
116
116
try await self . init ( registry: registryURL, client: urlsession, auth: auth)
117
117
}
118
+ }
118
119
119
- func registryURLForPath( _ path: String ) throws -> URL {
120
- var components = URLComponents ( )
121
- components. path = path
122
- guard let url = components. url ( relativeTo: registryURL) else {
123
- throw RegistryClientError . invalidRegistryPath ( path)
124
- }
125
- return url
120
+ extension URL {
121
+ /// The base distribution endpoint URL
122
+ var distributionEndpoint : URL { self . appendingPathComponent ( " /v2/ " ) }
123
+
124
+ /// The URL for a particular endpoint relating to a particular repository
125
+ /// - Parameters:
126
+ /// - repository: The name of the repository. May include path separators.
127
+ /// - endpoint: The distribution endpoint e.g. "tags/list"
128
+ func distributionEndpoint( forRepository repository: String , andEndpoint endpoint: String ) -> URL {
129
+ self . appendingPathComponent ( " /v2/ \( repository) / \( endpoint) " )
126
130
}
127
131
}
128
132
129
133
extension RegistryClient {
134
+ /// Represents an operation to be executed on the registry.
135
+ struct RegistryOperation {
136
+ enum Destination {
137
+ case subpath( String ) // Repository subpath on the registry
138
+ case url( URL ) // Full destination URL, for example from a Location header returned by the registry
139
+ }
140
+
141
+ var method : HTTPRequest . Method // HTTP method
142
+ var repository : String // Repository path on the registry
143
+ var destination : Destination // Destination of the operation: can be a subpath or remote URL
144
+ var accepting : [ String ] = [ ] // Acceptable response types
145
+ var contentType : String ? = nil // Request data type
146
+
147
+ func url( relativeTo registry: URL ) -> URL {
148
+ switch destination {
149
+ case . url( let url) : return url
150
+ case . subpath( let path) :
151
+ let subpath = registry. distributionEndpoint ( forRepository: repository, andEndpoint: path)
152
+ return subpath
153
+ }
154
+ }
155
+
156
+ // Convenience constructors
157
+ static func get(
158
+ _ repository: String ,
159
+ path: String ,
160
+ actions: [ String ] ? = nil ,
161
+ accepting: [ String ] = [ ] ,
162
+ contentType: String ? = nil
163
+ ) -> RegistryOperation {
164
+ . init(
165
+ method: . get,
166
+ repository: repository,
167
+ destination: . subpath( path) ,
168
+ accepting: accepting,
169
+ contentType: contentType
170
+ )
171
+ }
172
+
173
+ static func get(
174
+ _ repository: String ,
175
+ url: URL ,
176
+ actions: [ String ] ? = nil ,
177
+ accepting: [ String ] = [ ] ,
178
+ contentType: String ? = nil
179
+ ) -> RegistryOperation {
180
+ . init(
181
+ method: . get,
182
+ repository: repository,
183
+ destination: . url( url) ,
184
+ accepting: accepting,
185
+ contentType: contentType
186
+ )
187
+ }
188
+
189
+ static func head(
190
+ _ repository: String ,
191
+ path: String ,
192
+ actions: [ String ] ? = nil ,
193
+ accepting: [ String ] = [ ] ,
194
+ contentType: String ? = nil
195
+ ) -> RegistryOperation {
196
+ . init(
197
+ method: . head,
198
+ repository: repository,
199
+ destination: . subpath( path) ,
200
+ accepting: accepting,
201
+ contentType: contentType
202
+ )
203
+ }
204
+
205
+ /// 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
206
+ static func put(
207
+ _ repository: String ,
208
+ url: URL ,
209
+ actions: [ String ] ? = nil ,
210
+ accepting: [ String ] = [ ] ,
211
+ contentType: String ? = nil
212
+ ) -> RegistryOperation {
213
+ . init(
214
+ method: . put,
215
+ repository: repository,
216
+ destination: . url( url) ,
217
+ accepting: accepting,
218
+ contentType: contentType
219
+ )
220
+ }
221
+
222
+ static func put(
223
+ _ repository: String ,
224
+ path: String ,
225
+ actions: [ String ] ? = nil ,
226
+ accepting: [ String ] = [ ] ,
227
+ contentType: String ? = nil
228
+ ) -> RegistryOperation {
229
+ . init(
230
+ method: . put,
231
+ repository: repository,
232
+ destination: . subpath( path) ,
233
+ accepting: accepting,
234
+ contentType: contentType
235
+ )
236
+ }
237
+
238
+ static func post(
239
+ _ repository: String ,
240
+ path: String ,
241
+ actions: [ String ] ? = nil ,
242
+ accepting: [ String ] = [ ] ,
243
+ contentType: String ? = nil
244
+ ) -> RegistryOperation {
245
+ . init(
246
+ method: . post,
247
+ repository: repository,
248
+ destination: . subpath( path) ,
249
+ accepting: accepting,
250
+ contentType: contentType
251
+ )
252
+ }
253
+ }
254
+
130
255
/// Execute an HTTP request with no request body.
131
256
/// - Parameters:
132
- /// - request : The HTTP request to execute.
257
+ /// - operation : The Registry operation to execute.
133
258
/// - success: The HTTP status code expected if the request is successful.
134
259
/// - errors: Expected error codes for which the registry sends structured error messages.
135
260
/// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
136
261
/// - Throws: If the server response is unexpected or indicates that an error occurred.
137
262
///
138
263
/// A plain Data version of this function is required because Data is Decodable and decodes from base64.
139
264
/// Plain blobs are not encoded in the registry, so trying to decode them will fail.
140
- public func executeRequestThrowing(
141
- _ request : HTTPRequest ,
265
+ func executeRequestThrowing(
266
+ _ operation : RegistryOperation ,
142
267
expectingStatus success: HTTPResponse . Status = . ok,
143
268
decodingErrors errors: [ HTTPResponse . Status ]
144
269
) async throws -> ( data: Data , response: HTTPResponse ) {
270
+ let request = HTTPRequest (
271
+ method: operation. method,
272
+ url: operation. url ( relativeTo: registryURL) ,
273
+ accepting: operation. accepting,
274
+ contentType: operation. contentType
275
+ )
276
+
145
277
do {
146
278
let authenticatedRequest = auth? . auth ( for: request) ?? request
147
279
return try await client. executeRequestThrowing ( authenticatedRequest, expectingStatus: success)
@@ -166,8 +298,8 @@ extension RegistryClient {
166
298
/// - errors: Expected error codes for which the registry sends structured error messages.
167
299
/// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
168
300
/// - Throws: If the server response is unexpected or indicates that an error occurred.
169
- public func executeRequestThrowing< Response: Decodable > (
170
- _ request: HTTPRequest ,
301
+ func executeRequestThrowing< Response: Decodable > (
302
+ _ request: RegistryOperation ,
171
303
expectingStatus success: HTTPResponse . Status = . ok,
172
304
decodingErrors errors: [ HTTPResponse . Status ]
173
305
) async throws -> ( data: Response , response: HTTPResponse ) {
@@ -182,7 +314,7 @@ extension RegistryClient {
182
314
183
315
/// Execute an HTTP request uploading a request body.
184
316
/// - Parameters:
185
- /// - request : The HTTP request to execute.
317
+ /// - operation : The Registry operation to execute.
186
318
/// - payload: The request body to upload.
187
319
/// - success: The HTTP status code expected if the request is successful.
188
320
/// - errors: Expected error codes for which the registry sends structured error messages.
@@ -191,12 +323,19 @@ extension RegistryClient {
191
323
///
192
324
/// A plain Data version of this function is required because Data is Encodable and encodes to base64.
193
325
/// Accidentally encoding data blobs will cause digests to fail and runtimes to be unable to run the images.
194
- public func executeRequestThrowing(
195
- _ request : HTTPRequest ,
326
+ func executeRequestThrowing(
327
+ _ operation : RegistryOperation ,
196
328
uploading payload: Data ,
197
329
expectingStatus success: HTTPResponse . Status ,
198
330
decodingErrors errors: [ HTTPResponse . Status ]
199
331
) async throws -> ( data: Data , response: HTTPResponse ) {
332
+ let request = HTTPRequest (
333
+ method: operation. method,
334
+ url: operation. url ( relativeTo: registryURL) ,
335
+ accepting: operation. accepting,
336
+ contentType: operation. contentType
337
+ )
338
+
200
339
do {
201
340
let authenticatedRequest = auth? . auth ( for: request) ?? request
202
341
return try await client. executeRequestThrowing (
@@ -224,20 +363,20 @@ extension RegistryClient {
224
363
225
364
/// Execute an HTTP request uploading a Codable request body.
226
365
/// - Parameters:
227
- /// - request : The HTTP request to execute.
366
+ /// - operation : The Registry operation to execute.
228
367
/// - payload: The request body to upload.
229
368
/// - success: The HTTP status code expected if the request is successful.
230
369
/// - errors: Expected error codes for which the registry sends structured error messages.
231
370
/// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPURLResponse.
232
371
/// - Throws: If the server response is unexpected or indicates that an error occurred.
233
- public func executeRequestThrowing< Body: Encodable > (
234
- _ request : HTTPRequest ,
372
+ func executeRequestThrowing< Body: Encodable > (
373
+ _ operation : RegistryOperation ,
235
374
uploading payload: Body ,
236
375
expectingStatus success: HTTPResponse . Status ,
237
376
decodingErrors errors: [ HTTPResponse . Status ]
238
377
) async throws -> ( data: Data , response: HTTPResponse ) {
239
378
try await executeRequestThrowing (
240
- request ,
379
+ operation ,
241
380
uploading: try encoder. encode ( payload) ,
242
381
expectingStatus: success,
243
382
decodingErrors: errors
0 commit comments