Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGES/+head-based-manifest-version-check.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pull-through manifest resolution now uses HEAD checks, reducing Docker Hub 429s for repeated tag resolutions.
36 changes: 30 additions & 6 deletions pulp_container/app/registry_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1479,14 +1479,43 @@ def fetch_manifest(self, remote, pk):
Fetch a manifest from the upstream remote.
Returns the local manifest, if it exists in Pulp, and the full response from upstream.
Raises response errors if manifest is not found or the download fails.

A HEAD request is issued first to check the manifest's digest against what is already
stored in Pulp. Per Docker Hub's pull-rate accounting, a HEAD ("version check") does not
count as a pull, unlike a GET. If the manifest is already stored locally, the HEAD
response is returned as-is and the counted GET is skipped entirely.
"""
relative_url = "/v2/{name}/manifests/{pk}".format(
name=remote.namespaced_upstream_name, pk=pk
)
tag_url = urljoin(remote.url, relative_url)

head_response = self._fetch_manifest_response(remote, tag_url, pk, http_method="head")
digest = head_response.headers.get("docker-content-digest")
if digest:
manifest = models.Manifest.objects.filter(
digest=digest, pulp_domain=get_domain()
).first()
if manifest is not None:
return manifest, head_response

# The manifest is not stored locally yet, or the upstream did not report a digest on the
# HEAD response; fall back to a full GET, identical to the previous behavior.
response = self._fetch_manifest_response(remote, tag_url, pk, http_method="get")
digest = response.headers.get("docker-content-digest")
return models.Manifest.objects.filter(
digest=digest, pulp_domain=get_domain()
).first(), response

def _fetch_manifest_response(self, remote, tag_url, pk, http_method):
"""
Issue a HEAD or GET request for a manifest and map response errors consistently.
"""
downloader = remote.get_downloader(url=tag_url)
try:
response = downloader.fetch(extra_data={"headers": V2_ACCEPT_HEADERS})
return downloader.fetch(
extra_data={"headers": V2_ACCEPT_HEADERS, "http_method": http_method}
)
except ClientResponseError as response_error:
if response_error.status == 429:
# the client could request the manifest outside the docker hub pull limit;
Expand All @@ -1499,11 +1528,6 @@ def fetch_manifest(self, remote, pk):
except (ClientConnectionError, TimeoutException):
# The remote server is not available at the moment
raise GatewayTimeout()
else:
digest = response.headers.get("docker-content-digest")
return models.Manifest.objects.filter(
digest=digest, pulp_domain=get_domain()
).first(), response

def put(self, request, path, pk=None):
"""
Expand Down
Loading