From 5a346046fbd05fd3a46f4b6bf49afc008d6bf0f6 Mon Sep 17 00:00:00 2001 From: Gerrod Ubben Date: Wed, 1 Jul 2026 09:56:43 -0400 Subject: [PATCH] Add GET blob upload status endpoint Implement Docker v2 GET/HEAD on /v2//blobs/uploads/ to report resumable upload progress. Closes #483. Co-authored-by: Cursor --- CHANGES/483.feature | 1 + pulp_container/app/registry_api.py | 12 ++++++++ .../tests/functional/api/test_push_content.py | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 CHANGES/483.feature diff --git a/CHANGES/483.feature b/CHANGES/483.feature new file mode 100644 index 000000000..d751ba52a --- /dev/null +++ b/CHANGES/483.feature @@ -0,0 +1 @@ +Added support for retrieving blob upload status via `GET /v2//blobs/uploads/`. diff --git a/pulp_container/app/registry_api.py b/pulp_container/app/registry_api.py index d18b3964b..dca3edbba 100644 --- a/pulp_container/app/registry_api.py +++ b/pulp_container/app/registry_api.py @@ -1030,6 +1030,18 @@ def partial_update(self, request, path, pk=None): return UploadResponse(upload=upload, path=path, request=request) + def get(self, request, path, pk=None): + """ + Retrieve the status of an upload. + """ + _, repository = self.get_dr_push(request, path) + upload = get_object_or_404(models.Upload, repository=repository, pk=pk) + return UploadResponse(upload=upload, path=path, request=request, status=204) + + def head(self, request, path, pk=None): + """Respond to HEAD requests about blob uploads.""" + return self.get(request, path, pk=pk) + def put(self, request, path, pk=None): """ Create a blob from uploaded chunks. diff --git a/pulp_container/tests/functional/api/test_push_content.py b/pulp_container/tests/functional/api/test_push_content.py index 5041c58af..895af8509 100644 --- a/pulp_container/tests/functional/api/test_push_content.py +++ b/pulp_container/tests/functional/api/test_push_content.py @@ -1,6 +1,7 @@ """Tests that verify that images can be pushed to Pulp.""" import json +import uuid from subprocess import CalledProcessError from urllib.parse import urljoin @@ -640,3 +641,32 @@ def test_push_empty_manifest_list( assert manifest_list.media_type == MEDIA_TYPE.MANIFEST_LIST assert manifest_list.schema_version == 2 assert manifest_list.listed_manifests == [] + + +def test_blob_upload_status(local_registry, container_bindings, full_path, add_to_cleanup): + """Test GET blob upload status returns current upload progress.""" + namespace_name = str(uuid.uuid4()) + repo_name = f"{namespace_name}/upload_status" + upload_path = f"/v2/{full_path(repo_name)}/blobs/uploads/" + + response, auth = local_registry.get_response("POST", upload_path) + response.raise_for_status() + assert response.status_code == 202 + upload_uuid = response.headers["Docker-Upload-UUID"] + location = response.headers["Location"] + + namespace = container_bindings.PulpContainerNamespacesApi.list(name=namespace_name).results[0] + add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href) + + status_url = urljoin(container_bindings.client.configuration.host, location) + response = requests.get(status_url, auth=auth) + response.raise_for_status() + assert response.status_code == 204 + assert response.headers["Docker-Upload-UUID"] == upload_uuid + assert response.headers["Range"] == "0-0" + assert response.headers["Content-Length"] == "0" + + response = requests.head(status_url, auth=auth) + response.raise_for_status() + assert response.status_code == 204 + assert response.headers["Docker-Upload-UUID"] == upload_uuid