Skip to content

Commit 9550d81

Browse files
committed
Announce no metadata on pull-through links
A package not yet in the repository is linked as <file>?redirect=<upstream URL>, and the link announced its PEP 658 metadata. pip asks for that metadata by appending ".metadata" to the whole URL, so it lands in the redirect parameter and the path still names the package. Once another client has fetched the package, the content app finds it by that path and answers the metadata request with the package itself, and pip fails its hash check. The pull-through handler's workaround for pip's URL is never reached in that case, because the path lookup comes first. Pull-through links no longer announce metadata; installers then read it from the package they download. Once the package is in the repository, its local link announces the metadata again, and that link has no query string. The metadata is still served through a pull-through link for clients holding an older page. fixes #1453
1 parent 0a5065a commit 9550d81

3 files changed

Lines changed: 42 additions & 19 deletions

File tree

‎CHANGES/1453.bugfix‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stopped announcing PEP 658 metadata on pull-through links, whose ?redirect= query made pip ask for the metadata at the package's own path: once the package was cached, pip got the package in place of its metadata and failed its hash check.

‎pulp_python/app/pypi/views.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,18 @@ def _get_repo_version(path):
8181
return None
8282

8383

84+
# Part of the ETag of the simple pages. Change it whenever the pages render differently for
85+
# the same repository version, so a client revalidating a page cached before the change gets
86+
# the new page instead of a 304.
87+
SIMPLE_PAGE_REVISION = "pull-through-links-without-metadata"
88+
89+
8490
def _etag_func(request, path, **kwargs):
8591
"""Compute unquoted ETag for the condition decorator. Returns None if no repo."""
8692
repo_ver = _get_repo_version(path)
8793
if repo_ver is None:
8894
return None
89-
raw = f"{repo_ver.number}:{repo_ver.pulp_created.isoformat()}"
95+
raw = f"{repo_ver.number}:{repo_ver.pulp_created.isoformat()}:{SIMPLE_PAGE_REVISION}"
9096
return hashlib.sha256(raw.encode()).hexdigest()[:16]
9197

9298

@@ -378,7 +384,12 @@ def parse_package(release_package):
378384
"url": d_url,
379385
"sha256": release_package.digests.get("sha256", ""),
380386
"requires_python": release_package.requires_python,
381-
"metadata_sha256": (release_package.metadata_digests or {}).get("sha256"),
387+
# No PEP 658 metadata for a file served through this redirect link: pip asks for
388+
# it by appending ".metadata" to the whole URL, which leaves the path naming the
389+
# file itself. Once the file is in the repository, a client holding this link is
390+
# served the file by its path and gets it in place of the metadata. Installers
391+
# fall back to the metadata inside the file they download anyway.
392+
"metadata_sha256": None,
382393
"size": release_package.size,
383394
"upload_time": release_package.upload_time,
384395
"version": release_package.version,

‎pulp_python/tests/functional/api/test_full_mirror.py‎

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -208,36 +208,43 @@ def test_pull_through_filtering_bad_names(python_remote_factory, python_distribu
208208
@pytest.mark.parallel
209209
def test_pull_through_metadata(python_remote_factory, python_distribution_factory):
210210
"""
211-
Tests that metadata is correctly served when using pull-through.
211+
Tests that a pull-through link announces no metadata, but the metadata is still served.
212212
213-
So when requesting the metadata url according to PEP 658 you should just need to add .metadata
214-
to the end of the url path. Since pull-through includes a redirect query parameter we need to
215-
test adding .metadata to the end of the url path vs adding it to the end of redirect query.
213+
pip asks for PEP 658 metadata by appending ".metadata" to the end of the url, which puts it
214+
into the redirect query parameter and leaves the path naming the package. Once the package
215+
is in a repository, that request is answered with the package by its path, so the link must
216+
not announce metadata. A client holding an older page still gets the metadata either way.
216217
"""
217218
remote = python_remote_factory(includes=["pytz"])
218219
distro = python_distribution_factory(remote=remote.pulp_href)
219220

220221
url = f"{distro.base_url}simple/pytz/"
221222
project_page = ProjectPage.from_response(requests.get(url), "pytz")
223+
upstream_page = ProjectPage.from_response(
224+
requests.get(urljoin(PYPI_URL, "simple/pytz/")), "pytz"
225+
)
222226
filename1 = "pytz-2023.2-py2.py3-none-any.whl"
223227
filename2 = "pytz-2023.3-py2.py3-none-any.whl"
224228
package1 = next(p for p in project_page.packages if p.filename == filename1)
225229
package2 = next(p for p in project_page.packages if p.filename == filename2)
226-
assert package1.has_metadata
227-
assert package2.has_metadata
230+
assert "?redirect=" in package1.url
231+
assert not package1.has_metadata
232+
assert not package2.has_metadata
233+
upstream1 = next(p for p in upstream_page.packages if p.filename == filename1)
234+
upstream2 = next(p for p in upstream_page.packages if p.filename == filename2)
228235

229-
# The correct way to get the metadata url: add to path (uv does this)
236+
# Adding .metadata to the url path (uv does this)
230237
parts1 = urlsplit(package1.url)
231238
url1 = urlunsplit((parts1[0], parts1[1], parts1[2] + ".metadata", parts1[3], parts1[4]))
232239
r = requests.get(url1)
233240
assert r.status_code == 200
234-
assert sha256(r.content).hexdigest() == package1.metadata_digests["sha256"]
241+
assert sha256(r.content).hexdigest() == upstream1.metadata_digests["sha256"]
235242

236-
# The incorrect way to get the metadata url: add to end of string (pip does this)
243+
# Adding .metadata to the end of the url (pip does this)
237244
url2 = package2.url + ".metadata"
238245
r = requests.get(url2)
239246
assert r.status_code == 200
240-
assert sha256(r.content).hexdigest() == package2.metadata_digests["sha256"]
247+
assert sha256(r.content).hexdigest() == upstream2.metadata_digests["sha256"]
241248

242249

243250
@pytest.mark.parallel
@@ -247,7 +254,7 @@ def test_pull_through_metadata_with_repo(
247254
python_distribution_factory,
248255
pulpcore_bindings,
249256
):
250-
"""Tests that metadata is correctly saved when using pull-through with a repository."""
257+
"""Tests that metadata is correctly saved and announced when using pull-through with a repo."""
251258
remote = python_remote_factory(url=PYPI_URL, includes=["pip"])
252259
repo = python_repo_factory()
253260
distro = python_distribution_factory(repository=repo.pulp_href, remote=remote.pulp_href)
@@ -256,32 +263,36 @@ def test_pull_through_metadata_with_repo(
256263
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
257264
filename = "pip-26.0.1-py3-none-any.whl"
258265
package = next(p for p in project_page.packages if p.filename == filename)
259-
assert package.has_metadata
260266
assert "?redirect=" in package.url
267+
assert not package.has_metadata
268+
upstream_page = ProjectPage.from_response(requests.get(urljoin(PYPI_URL, "simple/pip/")), "pip")
269+
upstream = next(p for p in upstream_page.packages if p.filename == filename)
270+
metadata_sha256 = upstream.metadata_digests["sha256"]
261271

262272
# Retrieve the metadata and assert the content was not saved to the repository
263273
parts = urlsplit(package.url)
264274
url = urlunsplit((parts[0], parts[1], parts[2] + ".metadata", parts[3], parts[4]))
265275
r = requests.get(url)
266276
assert r.status_code == 200
267-
assert sha256(r.content).hexdigest() == package.metadata_digests["sha256"]
277+
assert sha256(r.content).hexdigest() == metadata_sha256
268278
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
269279
package = next(p for p in project_page.packages if p.filename == filename)
270-
assert package.has_metadata
271280
assert "?redirect=" in package.url
281+
assert not package.has_metadata
272282

273283
# Now retrieve the package and assert the content was saved with metadata
274284
r = requests.get(package.url)
275285
assert r.status_code == 200
276286
pa = pulpcore_bindings.ArtifactsApi.list(sha256=package.digests["sha256"])
277287
assert pa.count == 1
278-
ma = pulpcore_bindings.ArtifactsApi.list(sha256=package.metadata_digests["sha256"])
288+
ma = pulpcore_bindings.ArtifactsApi.list(sha256=metadata_sha256)
279289
assert ma.count == 1
280290

281-
# Check the simple page is updated to point to the local repository
291+
# Check the simple page is updated to point to the local repository, with the metadata
282292
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
283293
package = next(p for p in project_page.packages if p.filename == filename)
284294
assert "?redirect=" not in package.url
295+
assert package.metadata_digests["sha256"] == metadata_sha256
285296
r = requests.get(package.metadata_url)
286297
assert r.status_code == 200
287-
assert sha256(r.content).hexdigest() == package.metadata_digests["sha256"]
298+
assert sha256(r.content).hexdigest() == metadata_sha256

0 commit comments

Comments
 (0)