Skip to content

Commit 91f9b49

Browse files
authored
ContainerRegistry: The registry may return a relative blob upload URL (#44)
Motivation ---------- In the 'Post then Put' blob upload method (https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put) the client starts by making a POST request asking the registry to start an upload session. The registry responds with a URL to which the client should PUT the blob. The upload location might not be provided by the registry server, allowing the registry to offload storage to a different service. Until now all registries we have encountered have returned absolute upload URLs, however GHCR returns a relative URL causing uploads to fail as reported in #43. Modifications ------------- If the registry returns a relative URL, startBlobUpload() rewrites it to be an absolute URL referring to the registry. Result ------ Images can be pushed to GHCR and other registries which return relative upload locations. Test Plan --------- Automated tests continue to pass. Tested manually with GHCR and other known registries. Fixes: #43
1 parent 90642df commit 91f9b49

File tree

1 file changed

+20
-5
lines changed

1 file changed

+20
-5
lines changed

Sources/ContainerRegistry/Blobs.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ 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 -> URLComponents? {
31+
func startBlobUploadSession(repository: String) async throws -> URL {
3232
precondition(repository.count > 0, "repository must not be an empty string")
3333

3434
// Upload in "two shot" mode.
35+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put
3536
// - POST to obtain a session ID.
3637
// - Do not include the digest.
3738
// Response will include a 'Location' header telling us where to PUT the blob data.
@@ -44,7 +45,21 @@ extension RegistryClient {
4445
guard let location = httpResponse.response.headerFields[.location] else {
4546
throw HTTPClientError.missingResponseHeader("Location")
4647
}
47-
return URLComponents(string: location)
48+
49+
guard let locationURL = URL(string: location) else {
50+
throw RegistryClientError.invalidUploadLocation("\(location)")
51+
}
52+
53+
// The location may be either an absolute URL or a relative URL
54+
// If it is relative we need to make it absolute
55+
guard locationURL.host != nil else {
56+
guard let absoluteURL = URL(string: location, relativeTo: registryURL) else {
57+
throw RegistryClientError.invalidUploadLocation("\(location)")
58+
}
59+
return absoluteURL
60+
}
61+
62+
return locationURL
4863
}
4964
}
5065

@@ -123,14 +138,14 @@ public extension RegistryClient {
123138
precondition(repository.count > 0, "repository must not be an empty string")
124139

125140
// Ask the server to open a session and tell us where to upload our data
126-
var location = try await startBlobUploadSession(repository: repository)!
141+
let location = try await startBlobUploadSession(repository: repository)
127142

128143
// Append the digest to the upload location, as the specification requires.
129144
// The server's URL is arbitrary and might already contain query items which we must not overwrite.
130145
// The URL could even point to a different host.
131146
let digest = digest(of: data)
132-
location.queryItems = (location.queryItems ?? []) + [URLQueryItem(name: "digest", value: "\(digest.utf8)")]
133-
guard let uploadURL = location.url else { throw RegistryClientError.invalidUploadLocation("\(location)") }
147+
let uploadURL = location.appending(queryItems: [.init(name: "digest", value: "\(digest.utf8)")])
148+
134149
let httpResponse = try await executeRequestThrowing(
135150
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
136151
.put(repository, url: uploadURL, contentType: "application/octet-stream"),

0 commit comments

Comments
 (0)