Skip to content

ContainerRegistry: The registry may return a relative blob upload URL #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions Sources/ContainerRegistry/Blobs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ public func digest<D: DataProtocol>(of data: D) -> String {

extension RegistryClient {
// Internal helper method to initiate a blob upload in 'two shot' mode
func startBlobUploadSession(repository: String) async throws -> URLComponents? {
func startBlobUploadSession(repository: String) async throws -> URL {
precondition(repository.count > 0, "repository must not be an empty string")

// Upload in "two shot" mode.
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put
// - POST to obtain a session ID.
// - Do not include the digest.
// Response will include a 'Location' header telling us where to PUT the blob data.
Expand All @@ -44,7 +45,21 @@ extension RegistryClient {
guard let location = httpResponse.response.headerFields[.location] else {
throw HTTPClientError.missingResponseHeader("Location")
}
return URLComponents(string: location)

guard let locationURL = URL(string: location) else {
throw RegistryClientError.invalidUploadLocation("\(location)")
}

// The location may be either an absolute URL or a relative URL
// If it is relative we need to make it absolute
guard locationURL.host != nil else {
guard let absoluteURL = URL(string: location, relativeTo: registryURL) else {
throw RegistryClientError.invalidUploadLocation("\(location)")
}
return absoluteURL
}

return locationURL
}
}

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

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

// Append the digest to the upload location, as the specification requires.
// The server's URL is arbitrary and might already contain query items which we must not overwrite.
// The URL could even point to a different host.
let digest = digest(of: data)
location.queryItems = (location.queryItems ?? []) + [URLQueryItem(name: "digest", value: "\(digest.utf8)")]
guard let uploadURL = location.url else { throw RegistryClientError.invalidUploadLocation("\(location)") }
let uploadURL = location.appending(queryItems: [.init(name: "digest", value: "\(digest.utf8)")])

let httpResponse = try await executeRequestThrowing(
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
.put(repository, url: uploadURL, contentType: "application/octet-stream"),
Expand Down
Loading