Files
tart/Tests/TartTests/RegistryHostTests.swift
T
om singhal 65aea029ab 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.
2026-09-24 14:00:30 -07:00

89 lines
3.5 KiB
Swift

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 {
}
}