pulp-python 3.28.1__tar.gz → 3.29.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pulp_python-3.28.1 → pulp_python-3.29.0}/CHANGES.md +57 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/MANIFEST.in +2 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/PKG-INFO +1 -1
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.29.0/pulp_python/app/migrations/0022_pythonblocklistentry.py +48 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/models.py +64 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/serializers.py +122 -1
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/repair.py +25 -12
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/utils.py +35 -6
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/viewsets.py +78 -2
- pulp_python-3.29.0/pulp_python/tests/functional/api/test_blocklist.py +151 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/PKG-INFO +1 -1
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/SOURCES.txt +2 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pyproject.toml +2 -2
- {pulp_python-3.28.1 → pulp_python-3.29.0}/COMMITMENT +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/COPYRIGHT +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/LICENSE +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/README.md +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/functest_requirements.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/provenance.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/views.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/urls.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/requires.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/setup.cfg +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/test_requirements.txt +0 -0
- {pulp_python-3.28.1 → pulp_python-3.29.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,30 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.29.0 (2026-04-17) {: #3.29.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.29.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added repository-specific package blocklist.
|
|
16
|
+
[#1166](https://github.com/pulp/pulp_python/issues/1166)
|
|
17
|
+
|
|
18
|
+
#### Bugfixes {: #3.29.0-bugfix }
|
|
19
|
+
|
|
20
|
+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
|
|
21
|
+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
|
|
22
|
+
- Support "atomic" replications in pulpcore 3.107
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 3.28.2 (2026-04-14) {: #3.28.2 }
|
|
27
|
+
|
|
28
|
+
#### Bugfixes {: #3.28.2-bugfix }
|
|
29
|
+
|
|
30
|
+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
|
|
31
|
+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
11
35
|
## 3.28.1 (2026-04-01) {: #3.28.1 }
|
|
12
36
|
|
|
13
37
|
#### Bugfixes {: #3.28.1-bugfix }
|
|
@@ -33,6 +57,23 @@
|
|
|
33
57
|
|
|
34
58
|
---
|
|
35
59
|
|
|
60
|
+
## 3.27.2 (2026-04-14) {: #3.27.2 }
|
|
61
|
+
|
|
62
|
+
#### Bugfixes {: #3.27.2-bugfix }
|
|
63
|
+
|
|
64
|
+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
|
|
65
|
+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3.27.1 (2026-04-01) {: #3.27.1 }
|
|
70
|
+
|
|
71
|
+
#### Bugfixes {: #3.27.1-bugfix }
|
|
72
|
+
|
|
73
|
+
- Support "atomic" replications in pulpcore 3.107
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
36
77
|
## 3.27.0 (2026-03-17) {: #3.27.0 }
|
|
37
78
|
|
|
38
79
|
#### Bugfixes {: #3.27.0-bugfix }
|
|
@@ -340,6 +381,14 @@
|
|
|
340
381
|
|
|
341
382
|
---
|
|
342
383
|
|
|
384
|
+
## 3.13.6 (2026-04-01) {: #3.13.6 }
|
|
385
|
+
|
|
386
|
+
#### Bugfixes {: #3.13.6-bugfix }
|
|
387
|
+
|
|
388
|
+
- Support "atomic" replications in pulpcore 3.107
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
343
392
|
## 3.13.5 (2025-04-23) {: #3.13.5 }
|
|
344
393
|
|
|
345
394
|
No significant changes.
|
|
@@ -406,6 +455,14 @@ No significant changes.
|
|
|
406
455
|
|
|
407
456
|
---
|
|
408
457
|
|
|
458
|
+
## 3.12.9 (2026-04-01) {: #3.12.9 }
|
|
459
|
+
|
|
460
|
+
#### Bugfixes {: #3.12.9-bugfix }
|
|
461
|
+
|
|
462
|
+
- Support "atomic" replications in pulpcore 3.107
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
409
466
|
## 3.12.8 (2025-11-18) {: #3.12.8 }
|
|
410
467
|
|
|
411
468
|
No significant changes.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 5.2.10 on 2026-04-16 14:00
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_lifecycle.mixins
|
|
5
|
+
import pulpcore.app.models.base
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("python", "0021_pythonrepository_upload_duplicate_filenames"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="PythonBlocklistEntry",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"pulp_id",
|
|
21
|
+
models.UUIDField(
|
|
22
|
+
default=pulpcore.app.models.base.pulp_uuid,
|
|
23
|
+
editable=False,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
serialize=False,
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
("pulp_created", models.DateTimeField(auto_now_add=True)),
|
|
29
|
+
("pulp_last_updated", models.DateTimeField(auto_now=True, null=True)),
|
|
30
|
+
("name", models.TextField(default=None, null=True)),
|
|
31
|
+
("version", models.TextField(default=None, null=True)),
|
|
32
|
+
("filename", models.TextField(default=None, null=True)),
|
|
33
|
+
("added_by", models.TextField(default="")),
|
|
34
|
+
(
|
|
35
|
+
"repository",
|
|
36
|
+
models.ForeignKey(
|
|
37
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
38
|
+
related_name="blocklist_entries",
|
|
39
|
+
to="python.pythonrepository",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
options={
|
|
44
|
+
"default_related_name": "%(app_label)s_%(model_name)s",
|
|
45
|
+
},
|
|
46
|
+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
|
|
47
|
+
),
|
|
48
|
+
]
|
|
@@ -14,6 +14,7 @@ from django_lifecycle import (
|
|
|
14
14
|
from rest_framework.serializers import ValidationError
|
|
15
15
|
from pulpcore.plugin.models import (
|
|
16
16
|
AutoAddObjPermsMixin,
|
|
17
|
+
BaseModel,
|
|
17
18
|
Content,
|
|
18
19
|
Publication,
|
|
19
20
|
Distribution,
|
|
@@ -399,9 +400,12 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
399
400
|
|
|
400
401
|
When allow_package_substitution is False, reject any new version that would implicitly
|
|
401
402
|
replace existing content with different checksums (content substitution).
|
|
403
|
+
|
|
404
|
+
Also checks newly added content against the repository's blocklist entries.
|
|
402
405
|
"""
|
|
403
406
|
if not self.allow_package_substitution:
|
|
404
407
|
self._check_for_package_substitution(new_version)
|
|
408
|
+
self._check_blocklist(new_version)
|
|
405
409
|
remove_duplicates(new_version)
|
|
406
410
|
validate_repo_version(new_version)
|
|
407
411
|
|
|
@@ -418,3 +422,63 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
418
422
|
"To allow this, set 'allow_package_substitution' to True on the repository. "
|
|
419
423
|
f"Conflicting packages: {duplicates}"
|
|
420
424
|
)
|
|
425
|
+
|
|
426
|
+
def _check_blocklist(self, new_version):
|
|
427
|
+
"""
|
|
428
|
+
Check newly added content in a repository version against the blocklist.
|
|
429
|
+
"""
|
|
430
|
+
added_content = PythonPackageContent.objects.filter(
|
|
431
|
+
pk__in=new_version.added().values_list("pk", flat=True)
|
|
432
|
+
).only("filename", "name_normalized", "version")
|
|
433
|
+
if added_content.exists():
|
|
434
|
+
self.check_blocklist_for_packages(added_content)
|
|
435
|
+
|
|
436
|
+
def check_blocklist_for_packages(self, packages):
|
|
437
|
+
"""
|
|
438
|
+
Raise a ValidationError if any of the given packages match a blocklist entry.
|
|
439
|
+
"""
|
|
440
|
+
entries = PythonBlocklistEntry.objects.filter(repository=self)
|
|
441
|
+
if not entries.exists():
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
blocked = []
|
|
445
|
+
for pkg in packages:
|
|
446
|
+
for entry in entries:
|
|
447
|
+
if entry.filename and entry.filename == pkg.filename:
|
|
448
|
+
blocked.append(pkg.filename)
|
|
449
|
+
break
|
|
450
|
+
if entry.name == pkg.name_normalized:
|
|
451
|
+
if not entry.version or entry.version == pkg.version:
|
|
452
|
+
blocked.append(pkg.filename)
|
|
453
|
+
break
|
|
454
|
+
if blocked:
|
|
455
|
+
raise ValidationError(
|
|
456
|
+
"Blocklisted packages cannot be added to this repository: "
|
|
457
|
+
"{}".format(", ".join(blocked))
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class PythonBlocklistEntry(BaseModel):
|
|
462
|
+
"""
|
|
463
|
+
An entry in a PythonRepository's package blocklist.
|
|
464
|
+
|
|
465
|
+
Blocklist entries prevent packages from being added to the repository.
|
|
466
|
+
Entries can match by package `name` (all versions), package `name` + `version`,
|
|
467
|
+
or exact `filename`. Exactly one of `name` or `filename` must be provided.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
name = models.TextField(null=True, default=None)
|
|
471
|
+
version = models.TextField(null=True, default=None)
|
|
472
|
+
filename = models.TextField(null=True, default=None)
|
|
473
|
+
added_by = models.TextField(default="")
|
|
474
|
+
repository = models.ForeignKey(
|
|
475
|
+
PythonRepository, on_delete=models.CASCADE, related_name="blocklist_entries"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def __str__(self):
|
|
479
|
+
if self.filename:
|
|
480
|
+
return f"<{self._meta.object_name}: {self.filename}>"
|
|
481
|
+
return f"<{self._meta.object_name}: {self.name} [{self.version or 'all'}]>"
|
|
482
|
+
|
|
483
|
+
class Meta:
|
|
484
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
@@ -6,6 +6,7 @@ from django.conf import settings
|
|
|
6
6
|
from django.db.utils import IntegrityError
|
|
7
7
|
from drf_spectacular.utils import extend_schema_serializer
|
|
8
8
|
from packaging.requirements import Requirement
|
|
9
|
+
from packaging.version import Version, InvalidVersion
|
|
9
10
|
from rest_framework import serializers
|
|
10
11
|
from pypi_attestations import AttestationError
|
|
11
12
|
from pydantic import TypeAdapter, ValidationError
|
|
@@ -13,9 +14,10 @@ from urllib.parse import urljoin
|
|
|
13
14
|
|
|
14
15
|
from pulpcore.plugin import models as core_models
|
|
15
16
|
from pulpcore.plugin import serializers as core_serializers
|
|
16
|
-
from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user
|
|
17
|
+
from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user, reverse
|
|
17
18
|
|
|
18
19
|
from pulp_python.app import models as python_models
|
|
20
|
+
from pulp_python.app.utils import canonicalize_name
|
|
19
21
|
from pulp_python.app.provenance import (
|
|
20
22
|
Attestation,
|
|
21
23
|
Provenance,
|
|
@@ -53,6 +55,11 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
53
55
|
default=False,
|
|
54
56
|
required=False,
|
|
55
57
|
)
|
|
58
|
+
blocklist_entries_href = serializers.SerializerMethodField(
|
|
59
|
+
help_text=_("URL to the blocklist entries for this repository."),
|
|
60
|
+
read_only=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
56
63
|
allow_package_substitution = serializers.BooleanField(
|
|
57
64
|
help_text=_(
|
|
58
65
|
"Whether to allow package substitution (replacing existing packages with packages "
|
|
@@ -65,10 +72,15 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
65
72
|
required=False,
|
|
66
73
|
)
|
|
67
74
|
|
|
75
|
+
def get_blocklist_entries_href(self, obj):
|
|
76
|
+
repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.pk})
|
|
77
|
+
return f"{repo_href}blocklist_entries/"
|
|
78
|
+
|
|
68
79
|
class Meta:
|
|
69
80
|
fields = core_serializers.RepositorySerializer.Meta.fields + (
|
|
70
81
|
"autopublish",
|
|
71
82
|
"allow_package_substitution",
|
|
83
|
+
"blocklist_entries_href",
|
|
72
84
|
)
|
|
73
85
|
model = python_models.PythonRepository
|
|
74
86
|
|
|
@@ -780,6 +792,115 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
|
780
792
|
model = python_models.PythonRemote
|
|
781
793
|
|
|
782
794
|
|
|
795
|
+
class PythonBlocklistEntrySerializer(core_serializers.ModelSerializer):
|
|
796
|
+
"""
|
|
797
|
+
Serializer for PythonBlocklistEntry.
|
|
798
|
+
|
|
799
|
+
The `repository` is supplied by the URL (not the request body) and is injected
|
|
800
|
+
by the viewset before saving.
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
pulp_href = serializers.SerializerMethodField(
|
|
804
|
+
read_only=True,
|
|
805
|
+
help_text=_("The URL of this blocklist entry."),
|
|
806
|
+
)
|
|
807
|
+
repository = core_serializers.DetailRelatedField(
|
|
808
|
+
read_only=True,
|
|
809
|
+
view_name_pattern=r"repositories(-.*/.*)?-detail",
|
|
810
|
+
help_text=_("Repository this blocklist entry belongs to."),
|
|
811
|
+
)
|
|
812
|
+
name = serializers.CharField(
|
|
813
|
+
required=False,
|
|
814
|
+
allow_null=True,
|
|
815
|
+
default=None,
|
|
816
|
+
help_text=_(
|
|
817
|
+
"Package name to block (for all versions). Compared after PEP 503 normalization. "
|
|
818
|
+
"Required when 'filename' is not provided."
|
|
819
|
+
),
|
|
820
|
+
)
|
|
821
|
+
version = serializers.CharField(
|
|
822
|
+
required=False,
|
|
823
|
+
allow_null=True,
|
|
824
|
+
default=None,
|
|
825
|
+
help_text=_("Exact version string to block (e.g. '1.0'). Only used when 'name' is set."),
|
|
826
|
+
)
|
|
827
|
+
filename = serializers.CharField(
|
|
828
|
+
required=False,
|
|
829
|
+
allow_null=True,
|
|
830
|
+
default=None,
|
|
831
|
+
help_text=_("Exact filename to block. Required when 'name' is not provided."),
|
|
832
|
+
)
|
|
833
|
+
added_by = serializers.CharField(
|
|
834
|
+
read_only=True,
|
|
835
|
+
help_text=_("PRN of the user who added this blocklist entry."),
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
def get_pulp_href(self, obj):
|
|
839
|
+
repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.repository_id})
|
|
840
|
+
return f"{repo_href}blocklist_entries/{obj.pk}/"
|
|
841
|
+
|
|
842
|
+
def validate(self, data):
|
|
843
|
+
"""
|
|
844
|
+
Validate that the blocklist entry is well-formed and not a duplicate.
|
|
845
|
+
"""
|
|
846
|
+
name = data.get("name")
|
|
847
|
+
filename = data.get("filename")
|
|
848
|
+
version = data.get("version")
|
|
849
|
+
|
|
850
|
+
if version and filename:
|
|
851
|
+
raise serializers.ValidationError(_("'version' cannot be used with 'filename'."))
|
|
852
|
+
if version and not name:
|
|
853
|
+
raise serializers.ValidationError(_("'version' requires 'name' to be provided."))
|
|
854
|
+
if bool(name) == bool(filename):
|
|
855
|
+
raise serializers.ValidationError(
|
|
856
|
+
_("Exactly one of 'name' or 'filename' must be provided.")
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
if version:
|
|
860
|
+
try:
|
|
861
|
+
Version(version)
|
|
862
|
+
except InvalidVersion:
|
|
863
|
+
raise serializers.ValidationError(
|
|
864
|
+
{"version": _("'{}' is not a valid version.").format(version)}
|
|
865
|
+
)
|
|
866
|
+
if name:
|
|
867
|
+
data["name"] = canonicalize_name(name)
|
|
868
|
+
name = data["name"]
|
|
869
|
+
|
|
870
|
+
repository = self.context.get("repository")
|
|
871
|
+
if repository:
|
|
872
|
+
qs = python_models.PythonBlocklistEntry.objects.filter(repository=repository)
|
|
873
|
+
if name and qs.filter(name=name, version=version).exists():
|
|
874
|
+
raise serializers.ValidationError(
|
|
875
|
+
_("A blocklist entry with this name and version already exists.")
|
|
876
|
+
)
|
|
877
|
+
if filename and qs.filter(filename=filename).exists():
|
|
878
|
+
raise serializers.ValidationError(
|
|
879
|
+
_("A blocklist entry with this filename already exists.")
|
|
880
|
+
)
|
|
881
|
+
data["repository"] = repository
|
|
882
|
+
|
|
883
|
+
return data
|
|
884
|
+
|
|
885
|
+
def create(self, validated_data):
|
|
886
|
+
"""
|
|
887
|
+
Create a new blocklist entry, recording the authenticated user in `added_by`.
|
|
888
|
+
"""
|
|
889
|
+
user = get_current_authenticated_user()
|
|
890
|
+
validated_data["added_by"] = get_prn(user) if user else ""
|
|
891
|
+
return super().create(validated_data)
|
|
892
|
+
|
|
893
|
+
class Meta:
|
|
894
|
+
fields = core_serializers.ModelSerializer.Meta.fields + (
|
|
895
|
+
"repository",
|
|
896
|
+
"name",
|
|
897
|
+
"version",
|
|
898
|
+
"filename",
|
|
899
|
+
"added_by",
|
|
900
|
+
)
|
|
901
|
+
model = python_models.PythonBlocklistEntry
|
|
902
|
+
|
|
903
|
+
|
|
783
904
|
class PythonBanderRemoteSerializer(serializers.Serializer):
|
|
784
905
|
"""
|
|
785
906
|
A Serializer for the initial step of creating a Python Remote from a Bandersnatch config file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
from gettext import gettext as _
|
|
4
5
|
from itertools import groupby
|
|
@@ -8,18 +9,20 @@ from django.db.models import Prefetch
|
|
|
8
9
|
from django.db.models.query import QuerySet
|
|
9
10
|
from pulp_python.app.models import PythonPackageContent, PythonRepository
|
|
10
11
|
from pulp_python.app.utils import (
|
|
11
|
-
artifact_to_metadata_artifact,
|
|
12
12
|
artifact_to_python_content_data,
|
|
13
|
+
copy_artifact_to_temp_file,
|
|
14
|
+
extract_wheel_metadata,
|
|
13
15
|
fetch_json_release_metadata,
|
|
16
|
+
metadata_content_to_artifact,
|
|
14
17
|
parse_metadata,
|
|
15
18
|
)
|
|
16
|
-
from pulpcore.plugin.models import
|
|
19
|
+
from pulpcore.plugin.models import ContentArtifact, ProgressReport
|
|
17
20
|
from pulpcore.plugin.util import get_domain
|
|
18
21
|
|
|
19
22
|
log = logging.getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
BULK_SIZE =
|
|
25
|
+
BULK_SIZE = 250
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
def repair(repository_pk: UUID) -> None:
|
|
@@ -118,11 +121,21 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
|
|
|
118
121
|
.first()
|
|
119
122
|
.artifact
|
|
120
123
|
)
|
|
121
|
-
|
|
124
|
+
# Copy artifact to temp file once, extract both content data and metadata
|
|
125
|
+
temp_path = copy_artifact_to_temp_file(main_artifact, package.filename)
|
|
126
|
+
try:
|
|
127
|
+
new_data = artifact_to_python_content_data(
|
|
128
|
+
package.filename, main_artifact, domain, temp_path=temp_path
|
|
129
|
+
)
|
|
130
|
+
metadata_content = (
|
|
131
|
+
extract_wheel_metadata(temp_path) if package.filename.endswith(".whl") else None
|
|
132
|
+
)
|
|
133
|
+
finally:
|
|
134
|
+
os.unlink(temp_path)
|
|
122
135
|
total_metadata_repaired += update_metadata_artifact_if_needed(
|
|
123
136
|
package,
|
|
124
137
|
new_data.get("metadata_sha256"),
|
|
125
|
-
|
|
138
|
+
metadata_content,
|
|
126
139
|
metadata_batch,
|
|
127
140
|
pkgs_metadata_not_repaired,
|
|
128
141
|
)
|
|
@@ -236,7 +249,7 @@ def update_package_if_needed(
|
|
|
236
249
|
def update_metadata_artifact_if_needed(
|
|
237
250
|
package: PythonPackageContent,
|
|
238
251
|
new_metadata_sha256: str | None,
|
|
239
|
-
|
|
252
|
+
metadata_content: bytes | None,
|
|
240
253
|
metadata_batch: list[tuple],
|
|
241
254
|
pkgs_metadata_not_repaired: set[str],
|
|
242
255
|
) -> int:
|
|
@@ -248,7 +261,7 @@ def update_metadata_artifact_if_needed(
|
|
|
248
261
|
Args:
|
|
249
262
|
package: Package to check for metadata changes.
|
|
250
263
|
new_metadata_sha256: The correct metadata_sha256 extracted from the main artifact, or None.
|
|
251
|
-
|
|
264
|
+
metadata_content: Raw metadata bytes extracted from the wheel, or None.
|
|
252
265
|
metadata_batch: List of tuples for batch processing (updated in-place).
|
|
253
266
|
pkgs_metadata_not_repaired: Set of package PKs that failed repair (updated in-place).
|
|
254
267
|
|
|
@@ -265,13 +278,13 @@ def update_metadata_artifact_if_needed(
|
|
|
265
278
|
|
|
266
279
|
# Create missing
|
|
267
280
|
if not cas:
|
|
268
|
-
metadata_batch.append((package,
|
|
281
|
+
metadata_batch.append((package, metadata_content))
|
|
269
282
|
# Fix existing
|
|
270
283
|
elif new_metadata_sha256 != original_metadata_sha256:
|
|
271
284
|
ca = cas.first()
|
|
272
285
|
metadata_artifact = ca.artifact
|
|
273
286
|
if metadata_artifact is None or (metadata_artifact.sha256 != new_metadata_sha256):
|
|
274
|
-
metadata_batch.append((package,
|
|
287
|
+
metadata_batch.append((package, metadata_content))
|
|
275
288
|
|
|
276
289
|
if len(metadata_batch) == BULK_SIZE:
|
|
277
290
|
not_repaired = _process_metadata_batch(metadata_batch)
|
|
@@ -288,7 +301,7 @@ def _process_metadata_batch(metadata_batch: list[tuple]) -> set[str]:
|
|
|
288
301
|
and their corresponding ContentArtifacts.
|
|
289
302
|
|
|
290
303
|
Args:
|
|
291
|
-
metadata_batch: List of (package,
|
|
304
|
+
metadata_batch: List of (package, metadata_content) tuples.
|
|
292
305
|
|
|
293
306
|
Returns:
|
|
294
307
|
Set of package PKs for which metadata artifacts could not be created.
|
|
@@ -296,8 +309,8 @@ def _process_metadata_batch(metadata_batch: list[tuple]) -> set[str]:
|
|
|
296
309
|
not_repaired = set()
|
|
297
310
|
content_artifacts = []
|
|
298
311
|
|
|
299
|
-
for package,
|
|
300
|
-
metadata_artifact =
|
|
312
|
+
for package, metadata_content in metadata_batch:
|
|
313
|
+
metadata_artifact = metadata_content_to_artifact(metadata_content)
|
|
301
314
|
if metadata_artifact:
|
|
302
315
|
ca = ContentArtifact(
|
|
303
316
|
artifact=metadata_artifact,
|
|
@@ -240,18 +240,37 @@ def compute_metadata_sha256(filename: str) -> str | None:
|
|
|
240
240
|
return hashlib.sha256(metadata_content).hexdigest() if metadata_content else None
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
def
|
|
243
|
+
def copy_artifact_to_temp_file(artifact, filename, tmp_dir="."):
|
|
244
|
+
"""
|
|
245
|
+
Copy an artifact's file to a temporary file on disk.
|
|
246
|
+
|
|
247
|
+
Returns the path to the temp file. The caller is responsible for cleanup.
|
|
248
|
+
"""
|
|
249
|
+
temp_file = tempfile.NamedTemporaryFile("wb", dir=tmp_dir, suffix=filename, delete=False)
|
|
250
|
+
artifact.file.seek(0)
|
|
251
|
+
shutil.copyfileobj(artifact.file, temp_file)
|
|
252
|
+
temp_file.flush()
|
|
253
|
+
temp_file.close()
|
|
254
|
+
return temp_file.name
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def artifact_to_python_content_data(filename, artifact, domain=None, temp_path=None):
|
|
244
258
|
"""
|
|
245
259
|
Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
|
|
260
|
+
|
|
261
|
+
If temp_path is provided, uses it instead of copying the artifact to a new temp file.
|
|
246
262
|
"""
|
|
247
263
|
# Copy file to a temp directory under the user provided filename, we do this
|
|
248
264
|
# because pkginfo validates that the filename has a valid extension before
|
|
249
265
|
# reading it
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
if temp_path:
|
|
267
|
+
metadata = get_project_metadata_from_file(temp_path)
|
|
268
|
+
else:
|
|
269
|
+
with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file:
|
|
270
|
+
artifact.file.seek(0)
|
|
271
|
+
shutil.copyfileobj(artifact.file, temp_file)
|
|
272
|
+
temp_file.flush()
|
|
273
|
+
metadata = get_project_metadata_from_file(temp_file.name)
|
|
255
274
|
data = parse_project_metadata(vars(metadata))
|
|
256
275
|
data["sha256"] = artifact.sha256
|
|
257
276
|
data["size"] = artifact.size
|
|
@@ -280,6 +299,16 @@ def artifact_to_metadata_artifact(
|
|
|
280
299
|
if not metadata_content:
|
|
281
300
|
return None
|
|
282
301
|
|
|
302
|
+
return metadata_content_to_artifact(metadata_content, tmp_dir)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def metadata_content_to_artifact(metadata_content: bytes, tmp_dir: str = ".") -> Artifact | None:
|
|
306
|
+
"""
|
|
307
|
+
Creates an Artifact from raw metadata content bytes.
|
|
308
|
+
"""
|
|
309
|
+
if not metadata_content:
|
|
310
|
+
return None
|
|
311
|
+
|
|
283
312
|
with tempfile.NamedTemporaryFile(
|
|
284
313
|
"wb", dir=tmp_dir, suffix=".metadata", delete=False
|
|
285
314
|
) as temp_md:
|
|
@@ -7,6 +7,12 @@ from packaging.utils import canonicalize_name
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from rest_framework import status
|
|
9
9
|
from rest_framework.decorators import action
|
|
10
|
+
from rest_framework.mixins import (
|
|
11
|
+
CreateModelMixin,
|
|
12
|
+
DestroyModelMixin,
|
|
13
|
+
ListModelMixin,
|
|
14
|
+
RetrieveModelMixin,
|
|
15
|
+
)
|
|
10
16
|
from rest_framework.response import Response
|
|
11
17
|
from rest_framework.serializers import ValidationError
|
|
12
18
|
|
|
@@ -143,15 +149,20 @@ class PythonRepositoryViewSet(
|
|
|
143
149
|
If allow_package_substitution is False and the request is **only** adding packages, then a
|
|
144
150
|
package substitution check is performed to provide a quicker error response. Otherwise, the
|
|
145
151
|
check is delegated to the task.
|
|
152
|
+
|
|
153
|
+
Also performs an early blocklist check on added packages.
|
|
146
154
|
"""
|
|
147
155
|
repository = self.get_object()
|
|
156
|
+
add_content_units = request.data.get("add_content_units", [])
|
|
157
|
+
content_ids = [extract_pk(x) for x in add_content_units]
|
|
158
|
+
|
|
159
|
+
self._early_blocklist_check(repository, content_ids)
|
|
160
|
+
|
|
148
161
|
if not repository.allow_package_substitution:
|
|
149
162
|
remove_content_units = request.data.get("remove_content_units", [])
|
|
150
163
|
if remove_content_units or "base_version" in request.data:
|
|
151
164
|
return super().modify(request, pk)
|
|
152
165
|
rvc = repository.latest_version().content
|
|
153
|
-
add_content_units = request.data.get("add_content_units", [])
|
|
154
|
-
content_ids = [extract_pk(x) for x in add_content_units]
|
|
155
166
|
packages = (
|
|
156
167
|
python_models.PythonPackageContent.objects.filter(pk__in=content_ids)
|
|
157
168
|
.exclude(pk__in=rvc)
|
|
@@ -167,6 +178,17 @@ class PythonRepositoryViewSet(
|
|
|
167
178
|
)
|
|
168
179
|
return super().modify(request, pk)
|
|
169
180
|
|
|
181
|
+
def _early_blocklist_check(self, repository, content_ids):
|
|
182
|
+
"""
|
|
183
|
+
Raise early if any added packages match a blocklist entry.
|
|
184
|
+
"""
|
|
185
|
+
if not content_ids:
|
|
186
|
+
return
|
|
187
|
+
packages = python_models.PythonPackageContent.objects.filter(pk__in=content_ids).only(
|
|
188
|
+
"filename", "name_normalized", "version"
|
|
189
|
+
)
|
|
190
|
+
repository.check_blocklist_for_packages(packages)
|
|
191
|
+
|
|
170
192
|
@extend_schema(
|
|
171
193
|
summary="Repair metadata",
|
|
172
194
|
responses={202: AsyncOperationResponseSerializer},
|
|
@@ -216,6 +238,60 @@ class PythonRepositoryViewSet(
|
|
|
216
238
|
return core_viewsets.OperationPostponedResponse(result, request)
|
|
217
239
|
|
|
218
240
|
|
|
241
|
+
class PythonBlocklistEntryViewSet(
|
|
242
|
+
core_viewsets.NamedModelViewSet,
|
|
243
|
+
CreateModelMixin,
|
|
244
|
+
RetrieveModelMixin,
|
|
245
|
+
ListModelMixin,
|
|
246
|
+
DestroyModelMixin,
|
|
247
|
+
):
|
|
248
|
+
"""
|
|
249
|
+
ViewSet for managing blocklist entries on a PythonRepository.
|
|
250
|
+
|
|
251
|
+
Blocklist entries prevent packages from being added to the repository.
|
|
252
|
+
Entries can match by package `name` (all versions), package `name` + `version`,
|
|
253
|
+
or exact `filename`. Exactly one of `name` or `filename` must be provided.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
endpoint_name = "blocklist_entries"
|
|
257
|
+
router_lookup = "pythonblocklistentry"
|
|
258
|
+
parent_viewset = PythonRepositoryViewSet
|
|
259
|
+
parent_lookup_kwargs = {"repository_pk": "repository__pk"}
|
|
260
|
+
serializer_class = python_serializers.PythonBlocklistEntrySerializer
|
|
261
|
+
queryset = python_models.PythonBlocklistEntry.objects.all()
|
|
262
|
+
ordering = ("-pulp_created",)
|
|
263
|
+
|
|
264
|
+
DEFAULT_ACCESS_POLICY = {
|
|
265
|
+
"statements": [
|
|
266
|
+
{
|
|
267
|
+
"action": ["list", "retrieve"],
|
|
268
|
+
"principal": "authenticated",
|
|
269
|
+
"effect": "allow",
|
|
270
|
+
"condition": "has_repository_model_or_domain_or_obj_perms:python.view_pythonrepository", # noqa: E501
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"action": ["create", "destroy"],
|
|
274
|
+
"principal": "authenticated",
|
|
275
|
+
"effect": "allow",
|
|
276
|
+
"condition": [
|
|
277
|
+
"has_repository_model_or_domain_or_obj_perms:python.modify_pythonrepository",
|
|
278
|
+
"has_repository_model_or_domain_or_obj_perms:python.view_pythonrepository",
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
def get_serializer_context(self):
|
|
285
|
+
"""
|
|
286
|
+
Inject the parent repository into the serializer context so that `validate()` can check for
|
|
287
|
+
duplicate entries. The guard on `repository_pk` prevents errors during schema generation.
|
|
288
|
+
"""
|
|
289
|
+
context = super().get_serializer_context()
|
|
290
|
+
if self.kwargs.get("repository_pk"):
|
|
291
|
+
context["repository"] = self.get_parent_object()
|
|
292
|
+
return context
|
|
293
|
+
|
|
294
|
+
|
|
219
295
|
class PythonRepositoryVersionViewSet(core_viewsets.RepositoryVersionViewSet):
|
|
220
296
|
"""
|
|
221
297
|
PythonRepositoryVersion represents a single Python repository version.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pulpcore.tests.functional.utils import PulpTaskError
|
|
4
|
+
from pulp_python.tests.functional.constants import PYTHON_EGG_FILENAME, PYTHON_EGG_URL
|
|
5
|
+
|
|
6
|
+
CONTENT_BODY = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
|
|
7
|
+
BLOCKED_MSG = "Blocklisted packages cannot be added to this repository"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parallel
|
|
11
|
+
def test_crd_entry(python_bindings, python_repo):
|
|
12
|
+
"""
|
|
13
|
+
CRD operations on blocklist entries return correct fields and update the entry count.
|
|
14
|
+
"""
|
|
15
|
+
entries_data = [
|
|
16
|
+
({"name": "shelf-reader"}, "shelf-reader", None, None),
|
|
17
|
+
({"name": "shelf-reader", "version": "0.1"}, "shelf-reader", "0.1", None),
|
|
18
|
+
({"filename": PYTHON_EGG_FILENAME}, None, None, PYTHON_EGG_FILENAME),
|
|
19
|
+
]
|
|
20
|
+
for body_kwargs, name, version, filename in entries_data:
|
|
21
|
+
entry = python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
22
|
+
python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
|
|
23
|
+
)
|
|
24
|
+
assert entry.name == name
|
|
25
|
+
assert entry.version == version
|
|
26
|
+
assert entry.filename == filename
|
|
27
|
+
assert entry.added_by == "prn:auth.user:1"
|
|
28
|
+
assert entry.pulp_href is not None
|
|
29
|
+
assert entry.prn is not None
|
|
30
|
+
|
|
31
|
+
result = python_bindings.RepositoriesPythonBlocklistEntriesApi.list(python_repo.pulp_href)
|
|
32
|
+
assert result.count == 3
|
|
33
|
+
|
|
34
|
+
entry = result.results[0]
|
|
35
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.read(entry.pulp_href)
|
|
36
|
+
|
|
37
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.delete(entry.pulp_href)
|
|
38
|
+
result = python_bindings.RepositoriesPythonBlocklistEntriesApi.list(python_repo.pulp_href)
|
|
39
|
+
assert result.count == 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parallel
|
|
43
|
+
@pytest.mark.parametrize(
|
|
44
|
+
"body_kwargs, expected_msg",
|
|
45
|
+
[
|
|
46
|
+
({"name": "shelf-reader"}, "this name and version already exists"),
|
|
47
|
+
({"name": "shelf-reader", "version": "0.1"}, "this name and version already exists"),
|
|
48
|
+
({"filename": PYTHON_EGG_FILENAME}, "this filename already exists"),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
def test_duplicate_entry_rejected(python_bindings, python_repo, body_kwargs, expected_msg):
|
|
52
|
+
"""
|
|
53
|
+
Creating a duplicate entry should fail.
|
|
54
|
+
"""
|
|
55
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
56
|
+
python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
|
|
57
|
+
)
|
|
58
|
+
with pytest.raises(python_bindings.ApiException) as ctx:
|
|
59
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
60
|
+
python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
|
|
61
|
+
)
|
|
62
|
+
assert ctx.value.status == 400
|
|
63
|
+
assert expected_msg in ctx.value.body
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.parallel
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
"body_kwargs, expected_msg",
|
|
69
|
+
[
|
|
70
|
+
({"version": "0.1", "filename": PYTHON_EGG_FILENAME}, "version' cannot be used with"),
|
|
71
|
+
({"version": "0.1"}, "version' requires 'name'"),
|
|
72
|
+
({"name": "shelf-reader", "filename": PYTHON_EGG_FILENAME}, "Exactly one of"),
|
|
73
|
+
({}, "Exactly one of"),
|
|
74
|
+
({"name": "shelf-reader", "version": "not-a-version"}, "not a valid version"),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
def test_invalid_entry_rejected(python_bindings, python_repo, body_kwargs, expected_msg):
|
|
78
|
+
"""
|
|
79
|
+
Creating an entry with invalid data should fail.
|
|
80
|
+
"""
|
|
81
|
+
with pytest.raises(python_bindings.ApiException) as ctx:
|
|
82
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
83
|
+
python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
|
|
84
|
+
)
|
|
85
|
+
assert ctx.value.status == 400
|
|
86
|
+
assert expected_msg in ctx.value.body
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parallel
|
|
90
|
+
def test_upload_blocked(monitor_task, python_bindings, python_repo):
|
|
91
|
+
"""
|
|
92
|
+
Uploading a package matching a blocklist entry is rejected.
|
|
93
|
+
"""
|
|
94
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
95
|
+
python_repo.pulp_href,
|
|
96
|
+
python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="0.1"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
with pytest.raises(PulpTaskError) as exc:
|
|
100
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
101
|
+
repository=python_repo.pulp_href, **CONTENT_BODY
|
|
102
|
+
)
|
|
103
|
+
monitor_task(response.task)
|
|
104
|
+
assert BLOCKED_MSG in exc.value.task.error["description"]
|
|
105
|
+
|
|
106
|
+
repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
|
|
107
|
+
assert repo.latest_version_href.endswith("/0/")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.parallel
|
|
111
|
+
def test_upload_allowed(monitor_task, python_bindings, python_repo):
|
|
112
|
+
"""
|
|
113
|
+
Uploading a package is allowed when the blocklist entry targets a different version.
|
|
114
|
+
"""
|
|
115
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
116
|
+
python_repo.pulp_href,
|
|
117
|
+
python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="9.9"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
121
|
+
repository=python_repo.pulp_href, **CONTENT_BODY
|
|
122
|
+
)
|
|
123
|
+
monitor_task(response.task)
|
|
124
|
+
|
|
125
|
+
repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
|
|
126
|
+
assert repo.latest_version_href.endswith("/1/")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parallel
|
|
130
|
+
def test_modify_blocked(monitor_task, python_bindings, python_repo):
|
|
131
|
+
"""
|
|
132
|
+
Adding a blocklisted package via repository modify is rejected.
|
|
133
|
+
"""
|
|
134
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
135
|
+
python_repo.pulp_href,
|
|
136
|
+
python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="0.1"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
response = python_bindings.ContentPackagesApi.create(**CONTENT_BODY)
|
|
140
|
+
task = monitor_task(response.task)
|
|
141
|
+
content = python_bindings.ContentPackagesApi.read(task.created_resources[0])
|
|
142
|
+
|
|
143
|
+
with pytest.raises(python_bindings.ApiException) as exc:
|
|
144
|
+
python_bindings.RepositoriesPythonApi.modify(
|
|
145
|
+
python_repo.pulp_href, {"add_content_units": [content.pulp_href]}
|
|
146
|
+
)
|
|
147
|
+
assert exc.value.status == 400
|
|
148
|
+
assert BLOCKED_MSG in exc.value.body
|
|
149
|
+
|
|
150
|
+
repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
|
|
151
|
+
assert repo.latest_version_href.endswith("/0/")
|
|
@@ -52,6 +52,7 @@ pulp_python/app/migrations/0018_packageprovenance.py
|
|
|
52
52
|
pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py
|
|
53
53
|
pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py
|
|
54
54
|
pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py
|
|
55
|
+
pulp_python/app/migrations/0022_pythonblocklistentry.py
|
|
55
56
|
pulp_python/app/migrations/__init__.py
|
|
56
57
|
pulp_python/app/pypi/__init__.py
|
|
57
58
|
pulp_python/app/pypi/serializers.py
|
|
@@ -72,6 +73,7 @@ pulp_python/tests/functional/utils.py
|
|
|
72
73
|
pulp_python/tests/functional/api/__init__.py
|
|
73
74
|
pulp_python/tests/functional/api/test_attestations.py
|
|
74
75
|
pulp_python/tests/functional/api/test_auto_publish.py
|
|
76
|
+
pulp_python/tests/functional/api/test_blocklist.py
|
|
75
77
|
pulp_python/tests/functional/api/test_consume_content.py
|
|
76
78
|
pulp_python/tests/functional/api/test_crud_content_unit.py
|
|
77
79
|
pulp_python/tests/functional/api/test_crud_publications.py
|
|
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pulp-python"
|
|
10
|
-
version = "3.
|
|
10
|
+
version = "3.29.0"
|
|
11
11
|
description = "pulp-python plugin for the Pulp Project"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -79,7 +79,7 @@ ignore = [
|
|
|
79
79
|
[tool.bumpversion]
|
|
80
80
|
# This section is managed by the plugin template. Do not edit manually.
|
|
81
81
|
|
|
82
|
-
current_version = "3.
|
|
82
|
+
current_version = "3.29.0"
|
|
83
83
|
commit = false
|
|
84
84
|
tag = false
|
|
85
85
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0003_new_sync_filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0010_update_json_field.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0018_packageprovenance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_attestations.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_auto_publish.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_consume_content.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_content_unit.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_publications.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_remotes.py
RENAMED
|
File without changes
|
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_download_content.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_export_import.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_full_mirror.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_apis.py
RENAMED
|
File without changes
|
{pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|