mirror of
https://github.com/cirruslabs/tart.git
synced 2026-09-29 18:51:19 +02:00
Use registry-1.docker.io for Docker Hub's docker.io host (#1332)
* Use registry-1.docker.io for Docker Hub's docker.io host docker.io doesn't serve the registry API: https://docker.io/v2/ redirects to https://www.docker.com/, which URLSession follows, getting back an HTML page with HTTP 200. As a result, pushing fails with UnexpectedHTTPStatusCode("pushing blob (POST)", 200, ...), pulling fails to parse the manifest and "tart login docker.io" accepts any credentials, because ping() never gets an authentication challenge. Send the API requests for docker.io to registry-1.docker.io instead, while still using docker.io for the pushed image names and for the credentials lookup, so that credentials saved with "tart login docker.io" keep working. Fixes #1275 * Match docker.io case insensitively * Recognize Docker Hub with an explicit port * Parse the registry host once and keep it normalized Using the host exactly as specified for naming and credentials lookup changed the behavior for other registries too. For example, "127.0.0.1:05000" used to find credentials stored for "127.0.0.1:5000", but didn't anymore. Parse the URL once instead, take the normalized host and port from it like before, and only replace the URL's host with registry-1.docker.io for Docker Hub.
This commit is contained in:
@@ -111,27 +111,24 @@ struct TokenResponse: Decodable, Authentication {
|
||||
}
|
||||
|
||||
class Registry {
|
||||
private let baseURL: URL
|
||||
let baseURL: URL
|
||||
let namespace: String
|
||||
let credentialsProviders: [CredentialsProvider]
|
||||
let authenticationKeeper = AuthenticationKeeper()
|
||||
|
||||
var host: String? {
|
||||
guard let host = baseURL.host else { return nil }
|
||||
|
||||
if let port = baseURL.port {
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
// Host with an optional port (e.g. "127.0.0.1:5000"), which is used for naming
|
||||
// and credentials lookup. For Docker Hub it stays "docker.io", while baseURL
|
||||
// points to registry-1.docker.io.
|
||||
let host: String?
|
||||
|
||||
init(baseURL: URL,
|
||||
namespace: String,
|
||||
host: String? = nil,
|
||||
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()]
|
||||
) throws {
|
||||
self.baseURL = baseURL
|
||||
self.namespace = namespace
|
||||
self.host = host ?? Registry.hostWithPort(of: baseURL)
|
||||
self.credentialsProviders = credentialsProviders
|
||||
}
|
||||
|
||||
@@ -142,9 +139,9 @@ class Registry {
|
||||
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()]
|
||||
) throws {
|
||||
let proto = insecure ? "http" : "https"
|
||||
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
|
||||
var baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
|
||||
|
||||
guard let baseURL = baseURLComponents.url else {
|
||||
guard var baseURL = baseURLComponents.url else {
|
||||
var hint = ""
|
||||
|
||||
if host.hasPrefix("http://") || host.hasPrefix("https://") {
|
||||
@@ -154,7 +151,33 @@ class Registry {
|
||||
throw RuntimeError.ImproperlyFormattedHost(host, hint)
|
||||
}
|
||||
|
||||
try self.init(baseURL: baseURL, namespace: namespace, credentialsProviders: credentialsProviders)
|
||||
// Naming and credentials lookup use the host and port of the original URL,
|
||||
// so it's "docker.io" for Docker Hub and "127.0.0.1:5000" for "127.0.0.1:05000"
|
||||
let normalizedHost = Registry.hostWithPort(of: baseURL)
|
||||
|
||||
// Docker Hub serves its registry API from registry-1.docker.io, while docker.io,
|
||||
// the host used in image names, redirects to Docker's website. URLSession follows
|
||||
// these redirects, so we'd get an HTML page with HTTP 200 instead of an API
|
||||
// response, which breaks pushing, pulling and "tart login" credentials validation.
|
||||
//
|
||||
// Host names are case insensitive, and only the host is replaced,
|
||||
// so an explicit port like in "Docker.IO:443" is kept.
|
||||
if baseURLComponents.host?.lowercased() == "docker.io" {
|
||||
baseURLComponents.host = "registry-1.docker.io"
|
||||
baseURL = baseURLComponents.url!
|
||||
}
|
||||
|
||||
try self.init(baseURL: baseURL, namespace: namespace, host: normalizedHost, credentialsProviders: credentialsProviders)
|
||||
}
|
||||
|
||||
private static func hostWithPort(of url: URL) -> String? {
|
||||
guard let host = url.host else { return nil }
|
||||
|
||||
if let port = url.port {
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func ping() async throws {
|
||||
@@ -421,12 +444,8 @@ class Registry {
|
||||
await authenticationKeeper.set(try TokenResponse.parse(fromData: data))
|
||||
}
|
||||
|
||||
private func lookupCredentials() throws -> (String, String)? {
|
||||
var host = baseURL.host!
|
||||
|
||||
if let port = baseURL.port {
|
||||
host += ":\(port)"
|
||||
}
|
||||
func lookupCredentials() throws -> (String, String)? {
|
||||
let host = self.host!
|
||||
|
||||
for provider in credentialsProviders {
|
||||
do {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
final class RegistryHostTests: XCTestCase {
|
||||
func testDockerHub() throws {
|
||||
let credentialsProvider = RecordingCredentialsProvider()
|
||||
let registry = try Registry(host: "docker.io", namespace: "org/repo",
|
||||
credentialsProviders: [credentialsProvider])
|
||||
|
||||
// docker.io redirects to Docker's website, so the API
|
||||
// requests should go to registry-1.docker.io instead
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "https://registry-1.docker.io/v2/"))
|
||||
|
||||
// ...while naming and credentials lookup should still use the host specified by the user
|
||||
XCTAssertEqual(registry.host, "docker.io")
|
||||
XCTAssertNil(try registry.lookupCredentials())
|
||||
XCTAssertEqual(credentialsProvider.requestedHosts, ["docker.io"])
|
||||
}
|
||||
|
||||
func testDockerHubIsMatchedCaseInsensitively() throws {
|
||||
let credentialsProvider = RecordingCredentialsProvider()
|
||||
let registry = try Registry(host: "Docker.IO", namespace: "org/repo",
|
||||
credentialsProviders: [credentialsProvider])
|
||||
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "https://registry-1.docker.io/v2/"))
|
||||
XCTAssertEqual(registry.host, "Docker.IO")
|
||||
XCTAssertNil(try registry.lookupCredentials())
|
||||
XCTAssertEqual(credentialsProvider.requestedHosts, ["Docker.IO"])
|
||||
}
|
||||
|
||||
func testDockerHubWithExplicitPort() throws {
|
||||
for host in ["docker.io:443", "DOCKER.IO:443"] {
|
||||
let registry = try Registry(host: host, namespace: "org/repo")
|
||||
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "https://registry-1.docker.io:443/v2/"))
|
||||
XCTAssertEqual(registry.host, host)
|
||||
}
|
||||
}
|
||||
|
||||
func testOtherHostsAreUnchanged() throws {
|
||||
for host in ["ghcr.io", "index.docker.io", "registry-1.docker.io", "registry.hub.docker.com", "127.0.0.1:8080"] {
|
||||
let registry = try Registry(host: host, namespace: "org/repo")
|
||||
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "https://\(host)/v2/"))
|
||||
XCTAssertEqual(registry.host, host)
|
||||
}
|
||||
|
||||
let registry = try Registry(host: "127.0.0.1:5000", namespace: "org/repo", insecure: true)
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "http://127.0.0.1:5000/v2/"))
|
||||
XCTAssertEqual(registry.host, "127.0.0.1:5000")
|
||||
}
|
||||
|
||||
func testHostPortIsNormalized() throws {
|
||||
// Credentials stored for "127.0.0.1:5000" should still be found
|
||||
// when the port is written with a leading zero
|
||||
let credentialsProvider = RecordingCredentialsProvider(credentials: ["127.0.0.1:5000": ("user", "password")])
|
||||
let registry = try Registry(host: "127.0.0.1:05000", namespace: "org/repo", insecure: true,
|
||||
credentialsProviders: [credentialsProvider])
|
||||
|
||||
XCTAssertEqual(registry.baseURL, URL(string: "http://127.0.0.1:05000/v2/"))
|
||||
XCTAssertEqual(registry.host, "127.0.0.1:5000")
|
||||
|
||||
let (user, password) = try XCTUnwrap(registry.lookupCredentials())
|
||||
XCTAssertEqual(user, "user")
|
||||
XCTAssertEqual(password, "password")
|
||||
XCTAssertEqual(credentialsProvider.requestedHosts, ["127.0.0.1:5000"])
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class RecordingCredentialsProvider: CredentialsProvider {
|
||||
let userFriendlyName = "recording credentials provider"
|
||||
|
||||
let credentials: [String: (String, String)]
|
||||
var requestedHosts: [String] = []
|
||||
|
||||
init(credentials: [String: (String, String)] = [:]) {
|
||||
self.credentials = credentials
|
||||
}
|
||||
|
||||
func retrieve(host: String) throws -> (String, String)? {
|
||||
requestedHosts.append(host)
|
||||
|
||||
return credentials[host]
|
||||
}
|
||||
|
||||
func store(host: String, user: String, password: String) throws {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user