Skip to content

Commit 53c8d63

Browse files
committed
Fix Docker Hub base image pulling
- Translate docker.io to registry-1.docker.io for v2 API compatibility - Implement proper HTTP redirect handling for blob downloads (Docker Hub redirects to S3) - Add NoRedirect handler to manually manage redirects without auth headers for S3 - Enhance error messages with response body previews for debugging - Add test for docker.io registry translation - Fixes JSON decode error when pulling base images from Docker Hub
1 parent 10e99ea commit 53c8d63

File tree

2 files changed

+70
-9
lines changed

2 files changed

+70
-9
lines changed

src/pycontainer/registry_client.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
"""Docker Registry v2 API client implementation."""
2-
import urllib.request, urllib.parse, urllib.error, json, re, base64
2+
import urllib.request, urllib.parse, urllib.error, http.client, json, re, base64
33
from pathlib import Path
44
from typing import Optional, Dict, Tuple
55

6+
class NoRedirect(urllib.request.HTTPRedirectHandler):
7+
"""HTTP handler that doesn't follow redirects."""
8+
def redirect_request(self, req, fp, code, msg, headers, newurl):
9+
return None
10+
611
class RegistryClient:
712
def __init__(self, registry: str, repository: str, auth_token: Optional[str]=None, username: Optional[str]=None, password: Optional[str]=None):
813
self.registry=registry.rstrip('/')
14+
if self.registry=='docker.io':
15+
self.registry='registry-1.docker.io'
916
self.repository=repository
1017
self.auth_token=auth_token
1118
self.username=username
@@ -73,7 +80,13 @@ def _make_request(self, method: str, url: str, data: Optional[bytes]=None, heade
7380
self._bearer_token=self._get_bearer_token(auth_params)
7481
if self._bearer_token:
7582
return self._make_request(method, url, data, headers, retry_auth=False)
76-
return e.code, e.read(), dict(e.headers)
83+
body=b''
84+
try:
85+
body=e.read()
86+
except: pass
87+
return e.code, body, dict(e.headers)
88+
except Exception as ex:
89+
raise RuntimeError(f"Request failed for {url}: {ex}")
7790

7891
def blob_exists(self, digest: str) -> bool:
7992
"""Check if blob exists in registry via HEAD request."""
@@ -136,18 +149,61 @@ def pull_manifest(self, reference: str) -> Tuple[Dict, str]:
136149
headers={'Accept':'application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json'}
137150
status, body, resp_headers=self._make_request('GET', url, headers=headers)
138151
if status!=200:
139-
raise RuntimeError(f"Failed to pull manifest: {status} {body.decode()}")
152+
body_text=body.decode() if body else '(empty response)'
153+
raise RuntimeError(f"Failed to pull manifest: {status} {body_text}")
154+
if not body:
155+
raise RuntimeError(f"Empty response body from registry for {reference}")
156+
try:
157+
manifest=json.loads(body)
158+
except json.JSONDecodeError as e:
159+
body_preview=body[:500].decode('utf-8', errors='replace')
160+
raise RuntimeError(f"Invalid JSON in manifest response: {e}\nBody preview: {body_preview}")
140161
digest=resp_headers.get('Docker-Content-Digest') or resp_headers.get('docker-content-digest')
141-
return json.loads(body), digest
162+
return manifest, digest
142163

143164
def pull_blob(self, digest: str, dest_path: Path) -> bool:
144165
"""Download blob from registry to local path."""
145166
url=f"{self.base_url}/{self.repository}/blobs/{digest}"
146-
status, body, _=self._make_request('GET', url)
147-
if status!=200:
148-
raise RuntimeError(f"Failed to pull blob {digest}: {status}")
149-
dest_path.write_bytes(body)
150-
return True
167+
168+
req=urllib.request.Request(url)
169+
token=self._bearer_token or self.auth_token
170+
if token:
171+
req.add_header('Authorization', f'Bearer {token}')
172+
elif self.username and self.password:
173+
creds=base64.b64encode(f'{self.username}:{self.password}'.encode()).decode()
174+
req.add_header('Authorization', f'Basic {creds}')
175+
176+
opener=urllib.request.build_opener(NoRedirect)
177+
try:
178+
with opener.open(req) as resp:
179+
if resp.status in (301, 302, 303, 307, 308):
180+
redirect_url=resp.headers.get('Location')
181+
if redirect_url:
182+
redirect_req=urllib.request.Request(redirect_url)
183+
with urllib.request.urlopen(redirect_req) as redirect_resp:
184+
body=redirect_resp.read()
185+
dest_path.write_bytes(body)
186+
return True
187+
elif resp.status==200:
188+
body=resp.read()
189+
dest_path.write_bytes(body)
190+
return True
191+
else:
192+
body=resp.read()
193+
error_msg=body.decode('utf-8', errors='replace') if body else '(empty)'
194+
raise RuntimeError(f"Failed to pull blob {digest}: {resp.status} - {error_msg}")
195+
except urllib.error.HTTPError as e:
196+
if e.code in (301, 302, 303, 307, 308):
197+
redirect_url=e.headers.get('Location')
198+
if redirect_url:
199+
redirect_req=urllib.request.Request(redirect_url)
200+
with urllib.request.urlopen(redirect_req) as redirect_resp:
201+
body=redirect_resp.read()
202+
dest_path.write_bytes(body)
203+
return True
204+
body=e.read() if hasattr(e, 'read') else b''
205+
error_msg=body.decode('utf-8', errors='replace') if body else '(empty)'
206+
raise RuntimeError(f"Failed to pull blob {digest}: {e.code} - {error_msg}")
151207

152208
def parse_image_reference(ref: str) -> Tuple[str, str, str]:
153209
"""Parse image reference into (registry, repository, tag).

tests/test_registry_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def test_registry_client_construction():
2222

2323
client_auth=RegistryClient("localhost:5000","test","token123")
2424
assert client_auth.auth_token=="token123"
25+
26+
# Test docker.io translation to registry-1.docker.io
27+
client_dockerhub=RegistryClient("docker.io","library/python")
28+
assert client_dockerhub.registry=="registry-1.docker.io"
29+
assert client_dockerhub.base_url=="https://registry-1.docker.io/v2"
2530

2631
def test_registry_url_construction():
2732
"""Test URL construction for registry operations."""

0 commit comments

Comments
 (0)