pulp-container 2.26.16__tar.gz → 2.26.18__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.
Files changed (148) hide show
  1. {pulp_container-2.26.16 → pulp_container-2.26.18}/CHANGES.md +15 -0
  2. {pulp_container-2.26.16 → pulp_container-2.26.18}/PKG-INFO +2 -2
  3. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/__init__.py +1 -1
  4. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/authorization.py +2 -12
  5. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/cache.py +8 -8
  6. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/checks.py +30 -0
  7. pulp_container-2.26.18/pulp_container/app/management/commands/container-repair-pull-through-distributions.py +73 -0
  8. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/models.py +1 -3
  9. pulp_container-2.26.18/pulp_container/app/pull_through.py +69 -0
  10. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/registry_api.py +11 -13
  11. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/serializers.py +57 -15
  12. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/viewsets.py +24 -0
  13. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/constants.py +2 -0
  14. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_content_cache.py +1 -1
  15. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_domains.py +3 -2
  16. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_pull_through_cache.py +28 -10
  17. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_remote_filter_pull_through.py +3 -1
  18. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_tagging_images.py +3 -4
  19. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/conftest.py +60 -54
  20. pulp_container-2.26.18/pulp_container/tests/unit/test_pull_through_repair.py +142 -0
  21. pulp_container-2.26.18/pulp_container/tests/unit/test_serializers.py +280 -0
  22. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/PKG-INFO +2 -2
  23. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/SOURCES.txt +3 -0
  24. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/requires.txt +1 -1
  25. {pulp_container-2.26.16 → pulp_container-2.26.18}/pyproject.toml +5 -4
  26. pulp_container-2.26.16/pulp_container/tests/unit/test_serializers.py +0 -105
  27. {pulp_container-2.26.16 → pulp_container-2.26.18}/COMMITMENT +0 -0
  28. {pulp_container-2.26.16 → pulp_container-2.26.18}/COPYRIGHT +0 -0
  29. {pulp_container-2.26.16 → pulp_container-2.26.18}/LICENSE +0 -0
  30. {pulp_container-2.26.16 → pulp_container-2.26.18}/MANIFEST.in +0 -0
  31. {pulp_container-2.26.16 → pulp_container-2.26.18}/README.rst +0 -0
  32. {pulp_container-2.26.16 → pulp_container-2.26.18}/functest_requirements.txt +0 -0
  33. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/__init__.py +0 -0
  34. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/access_policy.py +0 -0
  35. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/content.py +0 -0
  36. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/downloaders.py +0 -0
  37. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/exceptions.py +0 -0
  38. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/fields.py +0 -0
  39. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/global_access_conditions.py +0 -0
  40. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/json_schemas.py +0 -0
  41. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/management/__init__.py +0 -0
  42. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/management/commands/__init__.py +0 -0
  43. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/management/commands/container-handle-image-data.py +0 -0
  44. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/management/commands/container-repair-media-type.py +0 -0
  45. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0001_initial.py +0 -0
  46. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0001_squashed_0032_upload_artifact.py +0 -0
  47. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0002_containerrepository.py +0 -0
  48. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0003_oci_mediatype.py +0 -0
  49. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0004_upload.py +0 -0
  50. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0005_contentredirectcontentguard.py +0 -0
  51. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0006_containerpushrepository.py +0 -0
  52. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0007_clear_tags_artifacts_refs.py +0 -0
  53. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0008_include_exclude_tags.py +0 -0
  54. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0009_container_namespace.py +0 -0
  55. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0010_remove_uploadchunk.py +0 -0
  56. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0011_add_container_repository_permissions.py +0 -0
  57. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0012_add_container_namespace_permissions.py +0 -0
  58. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0013_add_pull_push_permissions.py +0 -0
  59. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0014_containerdistribution_private.py +0 -0
  60. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0015_manage_tags_push_repo.py +0 -0
  61. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0016_add_delete_versions_permission.py +0 -0
  62. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0017_add_granular_perms.py +0 -0
  63. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0018_containerdistribution_description.py +0 -0
  64. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0019_DATA_distribution_model_swap.py +0 -0
  65. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0020_update_push_repo_perms.py +0 -0
  66. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0021_data_move_redirect_content_guard_to_core.py +0 -0
  67. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0022_delete_contentredirectcontentguard.py +0 -0
  68. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0023_manifestsignature.py +0 -0
  69. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0024_containerremote_sigstore.py +0 -0
  70. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0025_signature_stored_in_textfield.py +0 -0
  71. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0026_manifest_signing_service.py +0 -0
  72. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0027_data_translate_perms_to_roles.py +0 -0
  73. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0028_add_role_manage_permissions.py +0 -0
  74. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0029_remove_blob_media_type.py +0 -0
  75. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0030_enforce_tagged_manifest_reference.py +0 -0
  76. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0031_replace_charf_with_textf.py +0 -0
  77. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0032_upload_artifact.py +0 -0
  78. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0033_raise_warning_for_repair.py +0 -0
  79. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0034_translate_signed_schema.py +0 -0
  80. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0035_alter_blob_content_ptr_and_more.py +0 -0
  81. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0036_containerpushrepository_pending_blobs_manifests.py +0 -0
  82. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0037_create_pull_through_cache_models.py +0 -0
  83. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0038_add_manifest_metadata_fields.py +0 -0
  84. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0039_manifest_data.py +0 -0
  85. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0040_add_remote_repo_filter.py +0 -0
  86. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0041_add_pull_through_pull_permissions.py +0 -0
  87. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0042_add_manifest_nature_field.py +0 -0
  88. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0043_add_os_arch_image_size_manifest_fields.py +0 -0
  89. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0044_add_domain.py +0 -0
  90. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/0045_alter_manifest_compressed_image_size.py +0 -0
  91. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/migrations/__init__.py +0 -0
  92. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/modelresource.py +0 -0
  93. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/redirects.py +0 -0
  94. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/registry.py +0 -0
  95. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/replica.py +0 -0
  96. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/settings.py +0 -0
  97. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/__init__.py +0 -0
  98. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/builder.py +0 -0
  99. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/download_image_data.py +0 -0
  100. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/recursive_add.py +0 -0
  101. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/recursive_remove.py +0 -0
  102. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/sign.py +0 -0
  103. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/sync_stages.py +0 -0
  104. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/synchronize.py +0 -0
  105. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/tag.py +0 -0
  106. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/tasks/untag.py +0 -0
  107. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/token_verification.py +0 -0
  108. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/urls.py +0 -0
  109. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/utils.py +0 -0
  110. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/webserver_snippets/__init__.py +0 -0
  111. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/webserver_snippets/apache.conf +0 -0
  112. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/app/webserver_snippets/nginx.conf +0 -0
  113. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/__init__.py +0 -0
  114. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/__init__.py +0 -0
  115. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/__init__.py +0 -0
  116. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_build_images.py +0 -0
  117. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_crud_distributions.py +0 -0
  118. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_crud_remotes.py +0 -0
  119. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_flatpak.py +0 -0
  120. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_pull_content.py +0 -0
  121. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_pulpimportexport.py +0 -0
  122. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_push_content.py +0 -0
  123. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_push_signatures.py +0 -0
  124. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_rbac_push_repositories.py +0 -0
  125. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_rbac_remotes.py +0 -0
  126. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_rbac_repo_content.py +0 -0
  127. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_rbac_repo_versions.py +0 -0
  128. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_rbac_sync_repositories.py +0 -0
  129. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_recursive_add.py +0 -0
  130. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_recursive_remove.py +0 -0
  131. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_repositories_list.py +0 -0
  132. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_sign_manifests.py +0 -0
  133. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_sync.py +0 -0
  134. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_sync_signatures.py +0 -0
  135. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/api/test_token_authentication.py +0 -0
  136. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/constants.py +0 -0
  137. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/functional/utils.py +0 -0
  138. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/unit/__init__.py +0 -0
  139. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/unit/fixtures/ryuk_0.13.0_manifest_list.json +0 -0
  140. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/unit/test_json_schemas.py +0 -0
  141. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/unit/test_models.py +0 -0
  142. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container/tests/unit/test_sync_stages.py +0 -0
  143. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/dependency_links.txt +0 -0
  144. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/entry_points.txt +0 -0
  145. {pulp_container-2.26.16 → pulp_container-2.26.18}/pulp_container.egg-info/top_level.txt +0 -0
  146. {pulp_container-2.26.16 → pulp_container-2.26.18}/setup.cfg +0 -0
  147. {pulp_container-2.26.16 → pulp_container-2.26.18}/test_requirements.txt +0 -0
  148. {pulp_container-2.26.16 → pulp_container-2.26.18}/unittest_requirements.txt +0 -0
@@ -8,6 +8,21 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 2.26.18 (2026-10-07) {: #2.26.18 }
12
+
13
+ No significant changes.
14
+
15
+ ---
16
+
17
+ ## 2.26.17 (2026-09-23) {: #2.26.17 }
18
+
19
+ #### Bugfixes {: #2.26.17-bugfix }
20
+
21
+ - Prevent container distributions from overlapping other distribution base paths. Pull-through distributions **NEED** to be fixed before upgrading pulpcore; run `pulpcore-manager container-repair-pull-through-distributions` to repair existing distributions.
22
+ [#2494](https://github.com/pulp/pulp_container/issues/2494)
23
+
24
+ ---
25
+
11
26
  ## 2.26.16 (2026-06-08) {: #2.26.16 }
12
27
 
13
28
  #### Bugfixes {: #2.26.16-bugfix }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-container
3
- Version: 2.26.16
3
+ Version: 2.26.18
4
4
  Summary: Container plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -19,6 +19,6 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: jsonschema<4.25,>=4.4
21
21
  Requires-Dist: pulpcore<3.100,>=3.73.2
22
- Requires-Dist: pyjwt[crypto]<2.14,>=2.4
22
+ Requires-Dist: pyjwt[crypto]<2.16,>=2.4
23
23
  Requires-Dist: pysequoia<0.2.0,>=0.1.33
24
24
  Dynamic: license-file
@@ -24,7 +24,7 @@ class PulpContainerPluginAppConfig(PulpPluginAppConfig):
24
24
 
25
25
  name = "pulp_container.app"
26
26
  label = "container"
27
- version = "2.26.16"
27
+ version = "2.26.18"
28
28
  python_package_name = "pulp-container"
29
29
  domain_compatible = True
30
30
 
@@ -10,7 +10,6 @@ import jwt
10
10
  from cryptography.hazmat.backends import default_backend
11
11
  from cryptography.hazmat.primitives import serialization
12
12
  from django.conf import settings
13
- from django.db.models import F, Value
14
13
  from django.http import HttpRequest
15
14
  from rest_framework.request import Request
16
15
 
@@ -21,10 +20,10 @@ from pulp_container.app.access_policy import RegistryAccessPolicy
21
20
  from pulp_container.app.models import (
22
21
  ContainerDistribution,
23
22
  ContainerNamespace,
24
- ContainerPullThroughDistribution,
25
23
  )
24
+ from pulp_container.app.pull_through import get_pull_through_distribution
26
25
 
27
- TOKEN_EXPIRATION_TIME = settings.get("TOKEN_EXPIRATION_TIME", 300)
26
+ TOKEN_EXPIRATION_TIME = getattr(settings, "TOKEN_EXPIRATION_TIME", 300)
28
27
 
29
28
  FakeView = namedtuple("FakeView", "action, get_object, get_serializer")
30
29
 
@@ -209,15 +208,6 @@ class AuthorizationService:
209
208
  }
210
209
 
211
210
 
212
- def get_pull_through_distribution(path, domain):
213
- return (
214
- ContainerPullThroughDistribution.objects.annotate(path=Value(path))
215
- .filter(pulp_domain=domain, path__startswith=F("base_path"))
216
- .order_by("-base_path")
217
- .first()
218
- )
219
-
220
-
221
211
  class PermissionChecker:
222
212
  def __init__(self, user):
223
213
  self.user = user
@@ -1,11 +1,14 @@
1
1
  from django.core.exceptions import ObjectDoesNotExist
2
- from django.db.models import F, Value
3
2
 
4
3
  from pulpcore.plugin.cache import AsyncContentCache, CacheKeys, SyncContentCache
5
4
  from pulpcore.plugin.util import cache_key, get_domain
6
5
 
7
6
  from pulp_container.app.exceptions import RepositoryNotFound
8
- from pulp_container.app.models import ContainerDistribution, ContainerPullThroughDistribution
7
+ from pulp_container.app.models import ContainerDistribution
8
+ from pulp_container.app.pull_through import (
9
+ get_pull_through_distribution,
10
+ get_pull_through_distribution_path,
11
+ )
9
12
 
10
13
  ACCEPT_HEADER_KEY = "accept_header"
11
14
  QUERY_KEY = "query"
@@ -83,15 +86,12 @@ def find_base_path_cached(request, cached):
83
86
  try:
84
87
  distro = ContainerDistribution.objects.get(base_path=path, pulp_domain=domain)
85
88
  except ObjectDoesNotExist:
86
- distro = (
87
- ContainerPullThroughDistribution.objects.annotate(path=Value(path))
88
- .filter(path__startswith=F("base_path"), pulp_domain=domain)
89
- .order_by("-base_path")
90
- .first()
91
- )
89
+ distro = get_pull_through_distribution(path, domain)
92
90
  if not distro:
93
91
  raise RepositoryNotFound(name=path)
94
92
 
93
+ return cache_key(get_pull_through_distribution_path(distro))
94
+
95
95
  return cache_key(distro.base_path)
96
96
 
97
97
 
@@ -1,6 +1,11 @@
1
1
  from django.conf import settings
2
2
  from django.core.checks import Error as CheckError
3
+ from django.core.checks import Warning as CheckWarning
3
4
  from django.core.checks import register
5
+ from django.db import OperationalError, ProgrammingError
6
+
7
+ from pulp_container.app.models import ContainerPullThroughDistribution
8
+ from pulp_container.constants import PULL_THROUGH_DISTRIBUTION_LABEL
4
9
 
5
10
 
6
11
  @register(deploy=True)
@@ -45,3 +50,28 @@ def container_settings_check(app_configs, **kwargs):
45
50
  )
46
51
 
47
52
  return errors
53
+
54
+
55
+ @register()
56
+ def pull_through_distribution_check(app_configs, **kwargs):
57
+ """Warn when legacy pull-through distributions need repair."""
58
+ try:
59
+ needs_repair = ContainerPullThroughDistribution.objects.exclude(
60
+ pulp_labels__has_key=PULL_THROUGH_DISTRIBUTION_LABEL
61
+ ).exists()
62
+ except (OperationalError, ProgrammingError):
63
+ return []
64
+
65
+ if not needs_repair:
66
+ return []
67
+
68
+ return [
69
+ CheckWarning(
70
+ "Pull-through distributions need base-path repair.",
71
+ hint=(
72
+ "Run 'pulpcore-manager container-repair-pull-through-distributions' and "
73
+ "resolve any distributions it reports."
74
+ ),
75
+ id="pulp_container.W001",
76
+ )
77
+ ]
@@ -0,0 +1,73 @@
1
+ from gettext import gettext as _
2
+ from uuid import uuid4
3
+
4
+ from django.conf import settings
5
+ from django.core.management import BaseCommand
6
+ from django.db import transaction
7
+
8
+ from pulpcore.plugin.cache import SyncContentCache
9
+
10
+ from pulp_container.app.models import ContainerPullThroughDistribution
11
+ from pulp_container.app.pull_through import (
12
+ PULL_THROUGH_DISTRIBUTION_LABEL,
13
+ find_container_distribution_overlaps,
14
+ )
15
+
16
+
17
+ class Command(BaseCommand):
18
+ """Repair pull-through distributions whose registry path is stored as their base path."""
19
+
20
+ help = _("Repair pull-through distribution base paths and report remaining overlaps.")
21
+
22
+ def handle(self, *args, **options):
23
+ """Move eligible pull-through base paths and report unresolved overlaps."""
24
+ repaired = 0
25
+ migrated_base_paths_and_domain = []
26
+ distributions = ContainerPullThroughDistribution.objects.exclude(
27
+ pulp_labels__has_key=PULL_THROUGH_DISTRIBUTION_LABEL
28
+ ).select_related("pulp_domain")
29
+
30
+ for distribution in distributions.iterator():
31
+ old_base_path = distribution.base_path
32
+ distribution.base_path = str(uuid4())
33
+ distribution.pulp_labels = {
34
+ **(distribution.pulp_labels or {}),
35
+ PULL_THROUGH_DISTRIBUTION_LABEL: old_base_path,
36
+ }
37
+ migrated_base_paths_and_domain.append((old_base_path, distribution.pulp_domain))
38
+ with transaction.atomic():
39
+ distribution.save(update_fields=["base_path", "pulp_labels"])
40
+ repaired += 1
41
+
42
+ self.stdout.write(self.style.SUCCESS(f"Repaired {repaired} pull-through distributions."))
43
+
44
+ overlaps = find_container_distribution_overlaps()
45
+ if overlaps:
46
+ self.stdout.write(
47
+ self.style.WARNING(
48
+ "The following container distributions have overlapping base paths:"
49
+ )
50
+ )
51
+ for distribution, other in overlaps:
52
+ self.stdout.write(
53
+ f" {self._format_distribution(distribution).strip()} overlaps "
54
+ f"{self._format_distribution(other).strip()}"
55
+ )
56
+
57
+ if settings.CACHE_ENABLED and migrated_base_paths_and_domain:
58
+ if settings.DOMAIN_ENABLED:
59
+ old_base_paths = [
60
+ f"{domain.name}:{base_path}"
61
+ for base_path, domain in migrated_base_paths_and_domain
62
+ ]
63
+ else:
64
+ old_base_paths = [base_path for base_path, _ in migrated_base_paths_and_domain]
65
+ SyncContentCache().delete(base_key=old_base_paths)
66
+ self.stdout.write(self.style.SUCCESS("Flushed repaired distribution cache entries."))
67
+
68
+ @staticmethod
69
+ def _format_distribution(distribution):
70
+ return (
71
+ f" domain={distribution.pulp_domain.name!r} pk={distribution.pk} "
72
+ f"name={distribution.name!r} base_path={distribution.base_path!r}"
73
+ )
@@ -2,7 +2,6 @@ import json
2
2
  import os
3
3
  import re
4
4
  import tempfile
5
- import time
6
5
  from gettext import gettext as _
7
6
  from logging import getLogger
8
7
 
@@ -937,8 +936,7 @@ INCOMPLETE_EXT = ".part"
937
936
 
938
937
  def generate_filename(instance, filename):
939
938
  """Method for generating upload file name"""
940
- filename = os.path.join(instance.upload_dir, str(instance.pk) + INCOMPLETE_EXT)
941
- return time.strftime(filename)
939
+ return os.path.join(instance.upload_dir, str(instance.pk) + INCOMPLETE_EXT)
942
940
 
943
941
 
944
942
  class Upload(CoreUpload):
@@ -0,0 +1,69 @@
1
+ from django.db.models import F, Q, TextField, Value
2
+ from django.db.models.functions import Concat
3
+
4
+ from pulpcore.plugin.models import Distribution
5
+
6
+ from pulp_container.app.models import ContainerDistribution, ContainerPullThroughDistribution
7
+ from pulp_container.constants import (
8
+ PULL_THROUGH_DISTRIBUTION_LABEL,
9
+ )
10
+
11
+
12
+ def is_pull_through_distribution_marked(distribution):
13
+ """Return whether a pull-through distribution stores its registry path in pulp_labels."""
14
+ return (distribution.pulp_labels or {}).get(PULL_THROUGH_DISTRIBUTION_LABEL)
15
+
16
+
17
+ def get_pull_through_distribution_path(distribution):
18
+ """Return the registry path represented by a pull-through distribution."""
19
+ if base_path := is_pull_through_distribution_marked(distribution):
20
+ return base_path
21
+ return distribution.base_path
22
+
23
+
24
+ def _match_path(queryset, path, field):
25
+ """Return the longest segment-aware path match from a queryset."""
26
+ prefix_field = f"{field}_prefix"
27
+ return (
28
+ queryset.annotate(
29
+ requested_path=Value(path, output_field=TextField()),
30
+ **{prefix_field: Concat(F(field), Value("/"), output_field=TextField())},
31
+ )
32
+ .filter(Q(requested_path=F(field)) | Q(requested_path__startswith=F(prefix_field)))
33
+ .order_by(f"-{field}")
34
+ .first()
35
+ )
36
+
37
+
38
+ def get_pull_through_distribution(path, domain):
39
+ """Find a pull-through distribution using the repaired scheme, then the legacy scheme."""
40
+ distributions = ContainerPullThroughDistribution.objects.filter(pulp_domain=domain)
41
+ marked = distributions.filter(pulp_labels__has_key=PULL_THROUGH_DISTRIBUTION_LABEL)
42
+ if distribution := _match_path(marked, path, f"pulp_labels__{PULL_THROUGH_DISTRIBUTION_LABEL}"):
43
+ return distribution
44
+
45
+ legacy = distributions.exclude(pulp_labels__has_key=PULL_THROUGH_DISTRIBUTION_LABEL)
46
+ return _match_path(legacy, path, "base_path")
47
+
48
+
49
+ def find_container_distribution_overlaps():
50
+ """Return pairs where a container distribution overlaps any distribution."""
51
+ container_ids = set(ContainerDistribution.objects.values_list("pk", flat=True))
52
+ distributions = Distribution.objects.select_related("pulp_domain").order_by(
53
+ "pulp_domain_id", "base_path", "pk"
54
+ )
55
+ by_domain_and_path = {
56
+ (distribution.pulp_domain_id, distribution.base_path): distribution
57
+ for distribution in distributions.iterator()
58
+ }
59
+ overlaps = []
60
+
61
+ for distribution in by_domain_and_path.values():
62
+ parts = distribution.base_path.split("/")
63
+ for index in range(1, len(parts)):
64
+ parent_path = "/".join(parts[:index])
65
+ if parent := by_domain_and_path.get((distribution.pulp_domain_id, parent_path)):
66
+ if parent.pk in container_ids or distribution.pk in container_ids:
67
+ overlaps.append((parent, distribution))
68
+
69
+ return overlaps
@@ -66,6 +66,10 @@ from pulp_container.app.exceptions import (
66
66
  RepositoryInvalid,
67
67
  RepositoryNotFound,
68
68
  )
69
+ from pulp_container.app.pull_through import (
70
+ get_pull_through_distribution,
71
+ get_pull_through_distribution_path,
72
+ )
69
73
  from pulp_container.app.redirects import (
70
74
  AzureStorageRedirects,
71
75
  FileStorageRedirects,
@@ -230,7 +234,7 @@ class ContainerRegistryApiMixin:
230
234
  """
231
235
  List of authentication classes to check for this view.
232
236
  """
233
- if settings.get("TOKEN_AUTH_DISABLED", False):
237
+ if getattr(settings, "TOKEN_AUTH_DISABLED", False):
234
238
  return [RegistryAuthentication]
235
239
  return [TokenAuthentication]
236
240
 
@@ -239,7 +243,7 @@ class ContainerRegistryApiMixin:
239
243
  """
240
244
  List of permission classes to check for this view.
241
245
  """
242
- if settings.get("TOKEN_AUTH_DISABLED", False):
246
+ if getattr(settings, "TOKEN_AUTH_DISABLED", False):
243
247
  return [RegistryPermission]
244
248
  return [TokenPermission]
245
249
 
@@ -309,18 +313,12 @@ class ContainerRegistryApiMixin:
309
313
 
310
314
  def get_pull_through_drv(self, path):
311
315
  domain = get_domain()
312
- pull_through_cache_distribution = (
313
- models.ContainerPullThroughDistribution.objects.annotate(path=Value(path))
314
- .filter(path__startswith=F("base_path"), pulp_domain=domain)
315
- .order_by("-base_path")
316
- .first()
317
- )
316
+ pull_through_cache_distribution = get_pull_through_distribution(path, domain)
318
317
  if not pull_through_cache_distribution or not self.request.user.is_authenticated:
319
318
  raise RepositoryNotFound(name=path)
320
319
 
321
- upstream_name = path.split(pull_through_cache_distribution.base_path, maxsplit=1)[1].strip(
322
- "/"
323
- )
320
+ pull_through_path = get_pull_through_distribution_path(pull_through_cache_distribution)
321
+ upstream_name = path.split(pull_through_path, maxsplit=1)[1].strip("/")
324
322
  try:
325
323
  pull_through_remote = models.ContainerPullThroughRemote.objects.get(
326
324
  pk=pull_through_cache_distribution.remote_id
@@ -468,7 +466,7 @@ class VersionView(ContainerRegistryApiMixin, APIView):
468
466
  """
469
467
  List of permission classes to check for this view.
470
468
  """
471
- if settings.get("TOKEN_AUTH_DISABLED", False):
469
+ if getattr(settings, "TOKEN_AUTH_DISABLED", False):
472
470
  return [IsAuthenticated]
473
471
  return [TokenPermission]
474
472
 
@@ -560,7 +558,7 @@ class CatalogView(ContainerRegistryApiMixin, ListAPIView):
560
558
  distribution_permission = "container.pull_containerdistribution"
561
559
  namespace_permission = "container.namespace_pull_containerdistribution"
562
560
 
563
- if settings.get("TOKEN_AUTH_DISABLED", False):
561
+ if getattr(settings, "TOKEN_AUTH_DISABLED", False):
564
562
  return queryset
565
563
 
566
564
  public_repositories = queryset.filter(private=False)
@@ -1,5 +1,6 @@
1
1
  import re
2
2
  from gettext import gettext as _
3
+ from uuid import uuid4
3
4
 
4
5
  from django.core.validators import URLValidator
5
6
  from pulp_file.app.models import FileContent
@@ -32,13 +33,31 @@ from pulpcore.plugin.util import get_domain
32
33
 
33
34
  from pulp_container.app import fields, models
34
35
  from pulp_container.app.utils import get_full_path
35
- from pulp_container.constants import SIGNATURE_TYPE
36
+ from pulp_container.constants import (
37
+ PULL_THROUGH_DISTRIBUTION_LABEL,
38
+ SIGNATURE_TYPE,
39
+ )
36
40
 
37
41
  VALID_SIGNATURE_NAME_REGEX = r"^sha256:[0-9a-f]{64}@[0-9a-f]{32}$"
38
42
  VALID_TAG_REGEX = r"^[A-Za-z0-9][A-Za-z0-9._-]*$"
39
43
  VALID_BASE_PATH_REGEX_COMPILED = re.compile(r"^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9])*$")
40
44
 
41
45
 
46
+ def validate_registry_path(value):
47
+ """Validate a path that will be exposed by the container registry."""
48
+ if len(value) > 255:
49
+ raise serializers.ValidationError(
50
+ _("The entered registry path cannot be longer than 255 characters.")
51
+ )
52
+
53
+ if not all(re.match(VALID_BASE_PATH_REGEX_COMPILED, part) for part in value.split("/")):
54
+ raise serializers.ValidationError(
55
+ _("The provided registry path contains forbidden characters.")
56
+ )
57
+
58
+ return value
59
+
60
+
42
61
  class TagSerializer(NoArtifactContentSerializer):
43
62
  """
44
63
  Serializer for Tags.
@@ -456,17 +475,9 @@ class ContainerDistributionSerializer(DistributionSerializer, GetOrCreateSeriali
456
475
 
457
476
  def validate_base_path(self, value):
458
477
  """Check whether the passed repository base path is valid or not."""
459
- if len(value) > 255:
460
- raise serializers.ValidationError(
461
- _("The entered base path cannot be longer than 255 characters.")
462
- )
463
-
464
- if not all(re.match(VALID_BASE_PATH_REGEX_COMPILED, p) for p in value.split("/")):
465
- raise serializers.ValidationError(
466
- _("The provided base path contains forbidden characters.")
467
- )
468
-
469
- return value
478
+ validate_registry_path(value)
479
+ # Ensure we follow pulpcore's cross-distribution overlap validation in addition to the OCI rules.
480
+ return super().validate_base_path(value)
470
481
 
471
482
  class Meta:
472
483
  model = models.ContainerDistribution
@@ -513,18 +524,49 @@ class ContainerPullThroughDistributionSerializer(DistributionSerializer):
513
524
  description = serializers.CharField(
514
525
  help_text=_("An optional description."), required=False, allow_null=True
515
526
  )
527
+ base_path = serializers.CharField(
528
+ help_text=_("The registry path exposed by this pull-through distribution.")
529
+ )
530
+
531
+ def validate_base_path(self, value):
532
+ """Check whether the passed repository base path is valid or not."""
533
+ validate_registry_path(value)
534
+ # Ensure we follow pulpcore's cross-distribution overlap validation in addition to the OCI rules.
535
+ return super().validate_base_path(value)
516
536
 
517
537
  def validate(self, data):
518
538
  validated_data = super().validate(data)
539
+ registry_path = validated_data.get("base_path")
540
+
541
+ # This is the temporary fix for pull-through distros violating core's overlapping base-path rule
542
+ # Save the base_path in pulp_labels to avoid overlapping base-paths with this PT's child distros
543
+ # A permanent fix should have us move off this field or Distributions all together.
544
+ if self.instance is None:
545
+ base_path = validated_data.pop("base_path")
546
+ validated_data["base_path"] = str(uuid4())
547
+ validated_data.setdefault("pulp_labels", {})[PULL_THROUGH_DISTRIBUTION_LABEL] = (
548
+ base_path
549
+ )
550
+ elif (
551
+ "base_path" in validated_data and validated_data["base_path"] != self.instance.base_path
552
+ ):
553
+ is_marked = (self.instance.pulp_labels or {}).get(PULL_THROUGH_DISTRIBUTION_LABEL)
554
+ if is_marked:
555
+ message = _("This value cannot be updated.")
556
+ else:
557
+ message = _(
558
+ "Run 'pulpcore-manager container-repair-pull-through-distributions' before "
559
+ "updating this value."
560
+ )
561
+ raise serializers.ValidationError({"base_path": message})
519
562
 
520
563
  if "content_guard" not in validated_data:
521
564
  validated_data["content_guard"] = ContentRedirectContentGuardSerializer.get_or_create(
522
565
  {"name": "content redirect", "pulp_domain": get_domain()}
523
566
  )
524
567
 
525
- base_path = validated_data.get("base_path")
526
- if base_path:
527
- namespace_name = base_path.split("/")[0]
568
+ if registry_path:
569
+ namespace_name = registry_path.split("/")[0]
528
570
  validated_data["namespace"] = ContainerNamespaceSerializer.get_or_create(
529
571
  {"name": namespace_name, "pulp_domain": get_domain()}
530
572
  )
@@ -12,6 +12,7 @@ from django_filters import CharFilter, MultipleChoiceFilter
12
12
  from drf_spectacular.utils import extend_schema
13
13
  from rest_framework import mixins
14
14
  from rest_framework.decorators import action
15
+ from rest_framework.exceptions import ValidationError
15
16
 
16
17
  from pulpcore.plugin.models import Content, PulpTemporaryFile, RepositoryVersion
17
18
  from pulpcore.plugin.serializers import AsyncOperationResponseSerializer
@@ -41,6 +42,7 @@ from pulpcore.plugin.viewsets import (
41
42
  )
42
43
 
43
44
  from pulp_container.app import models, serializers, tasks
45
+ from pulp_container.constants import PULL_THROUGH_DISTRIBUTION_LABEL
44
46
 
45
47
  log = logging.getLogger(__name__)
46
48
 
@@ -1541,6 +1543,28 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin):
1541
1543
  ],
1542
1544
  }
1543
1545
 
1546
+ @action(
1547
+ detail=True,
1548
+ methods=["post"],
1549
+ serializer_class=DistributionViewSet.set_label.kwargs["serializer_class"],
1550
+ )
1551
+ def set_label(self, request, pk=None, **kwargs):
1552
+ """Prevent clients from changing the internal pull-through marker."""
1553
+ if request.data.get("key") == PULL_THROUGH_DISTRIBUTION_LABEL:
1554
+ raise ValidationError("This label is managed by pulp_container.")
1555
+ return super().set_label(request, pk, **kwargs)
1556
+
1557
+ @action(
1558
+ detail=True,
1559
+ methods=["post"],
1560
+ serializer_class=DistributionViewSet.unset_label.kwargs["serializer_class"],
1561
+ )
1562
+ def unset_label(self, request, pk=None, **kwargs):
1563
+ """Prevent clients from removing the internal pull-through marker."""
1564
+ if request.data.get("key") == PULL_THROUGH_DISTRIBUTION_LABEL:
1565
+ raise ValidationError("This label is managed by pulp_container.")
1566
+ return super().unset_label(request, pk, **kwargs)
1567
+
1544
1568
 
1545
1569
  class ContainerNamespaceViewSet(
1546
1570
  NamedModelViewSet,
@@ -75,6 +75,8 @@ SIGNATURE_PAYLOAD_MAX_SIZE = 4 * MEGABYTE
75
75
 
76
76
  SIGNATURE_API_EXTENSION_VERSION = 2
77
77
 
78
+ PULL_THROUGH_DISTRIBUTION_LABEL = "pulp_container.pull_through"
79
+
78
80
  MANIFEST_TYPE = SimpleNamespace(
79
81
  ARTIFACT="artifact",
80
82
  BOOTABLE="bootable",
@@ -76,7 +76,7 @@ def test_content_cache(
76
76
  monitor_task,
77
77
  ):
78
78
  """A test case that verifies the functionality of the Redis caching machinery."""
79
- if not pulp_settings.get("CACHE_ENABLED"):
79
+ if not getattr(pulp_settings, "CACHE_ENABLED", False):
80
80
  pytest.skip("The caching machinery was not enabled")
81
81
 
82
82
  repo = container_repository_factory()
@@ -222,11 +222,12 @@ def test_cross_domain_pulp_apis(
222
222
  },
223
223
  pulp_domain=domain1.name,
224
224
  )
225
+ pt_distribution_name = str(uuid.uuid4())
225
226
  with pytest.raises(container_bindings.ApiException) as e:
226
227
  container_bindings.DistributionsPullThroughApi.create(
227
228
  {
228
- "name": str(uuid.uuid4()),
229
- "base_path": str(uuid.uuid4()),
229
+ "name": pt_distribution_name,
230
+ "base_path": pt_distribution_name,
230
231
  "remote": pt_remote.pulp_href,
231
232
  },
232
233
  pulp_domain=domain2.name,