pulp-container 2.29.0__tar.gz → 2.29.2__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 (161) hide show
  1. {pulp_container-2.29.0 → pulp_container-2.29.2}/CHANGES.md +17 -0
  2. {pulp_container-2.29.0 → pulp_container-2.29.2}/PKG-INFO +4 -4
  3. {pulp_container-2.29.0 → pulp_container-2.29.2}/functest_requirements.txt +1 -2
  4. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/__init__.py +1 -1
  5. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/authorization.py +1 -11
  6. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/cache.py +10 -10
  7. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/checks.py +30 -0
  8. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/downloaders.py +26 -14
  9. pulp_container-2.29.2/pulp_container/app/management/commands/container-repair-pull-through-distributions.py +73 -0
  10. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/models.py +3 -4
  11. pulp_container-2.29.2/pulp_container/app/pull_through.py +69 -0
  12. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/registry_api.py +7 -9
  13. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/serializers.py +57 -15
  14. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/viewsets.py +24 -0
  15. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/constants.py +2 -0
  16. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_domains.py +3 -2
  17. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_pull_through_cache.py +28 -10
  18. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_push_content.py +0 -1
  19. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_push_signatures.py +44 -22
  20. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_remote_filter_pull_through.py +3 -1
  21. pulp_container-2.29.2/pulp_container/tests/functional/api/test_sign_manifests.py +125 -0
  22. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_tagging_images.py +3 -4
  23. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/conftest.py +151 -54
  24. pulp_container-2.29.2/pulp_container/tests/unit/test_pull_through_repair.py +142 -0
  25. pulp_container-2.29.2/pulp_container/tests/unit/test_serializers.py +280 -0
  26. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container.egg-info/PKG-INFO +4 -4
  27. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container.egg-info/SOURCES.txt +3 -0
  28. pulp_container-2.29.2/pulp_container.egg-info/requires.txt +4 -0
  29. {pulp_container-2.29.0 → pulp_container-2.29.2}/pyproject.toml +5 -5
  30. pulp_container-2.29.0/pulp_container/tests/functional/api/test_sign_manifests.py +0 -57
  31. pulp_container-2.29.0/pulp_container/tests/unit/test_serializers.py +0 -105
  32. pulp_container-2.29.0/pulp_container.egg-info/requires.txt +0 -4
  33. {pulp_container-2.29.0 → pulp_container-2.29.2}/COMMITMENT +0 -0
  34. {pulp_container-2.29.0 → pulp_container-2.29.2}/COPYRIGHT +0 -0
  35. {pulp_container-2.29.0 → pulp_container-2.29.2}/LICENSE +0 -0
  36. {pulp_container-2.29.0 → pulp_container-2.29.2}/MANIFEST.in +0 -0
  37. {pulp_container-2.29.0 → pulp_container-2.29.2}/README.rst +0 -0
  38. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/__init__.py +0 -0
  39. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/access_policy.py +0 -0
  40. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/content.py +0 -0
  41. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/dynaconf_hooks.py +0 -0
  42. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/exceptions.py +0 -0
  43. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/fields.py +0 -0
  44. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/global_access_conditions.py +0 -0
  45. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/json_schemas.py +0 -0
  46. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/management/__init__.py +0 -0
  47. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/management/commands/__init__.py +0 -0
  48. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/management/commands/container-handle-image-data.py +0 -0
  49. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/management/commands/container-repair-media-type.py +0 -0
  50. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0001_initial.py +0 -0
  51. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0001_squashed_0032_upload_artifact.py +0 -0
  52. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0002_containerrepository.py +0 -0
  53. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0003_oci_mediatype.py +0 -0
  54. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0004_upload.py +0 -0
  55. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0005_contentredirectcontentguard.py +0 -0
  56. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0006_containerpushrepository.py +0 -0
  57. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0007_clear_tags_artifacts_refs.py +0 -0
  58. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0008_include_exclude_tags.py +0 -0
  59. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0009_container_namespace.py +0 -0
  60. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0010_remove_uploadchunk.py +0 -0
  61. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0011_add_container_repository_permissions.py +0 -0
  62. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0012_add_container_namespace_permissions.py +0 -0
  63. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0013_add_pull_push_permissions.py +0 -0
  64. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0014_containerdistribution_private.py +0 -0
  65. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0015_manage_tags_push_repo.py +0 -0
  66. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0016_add_delete_versions_permission.py +0 -0
  67. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0017_add_granular_perms.py +0 -0
  68. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0018_containerdistribution_description.py +0 -0
  69. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0019_DATA_distribution_model_swap.py +0 -0
  70. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0020_update_push_repo_perms.py +0 -0
  71. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0021_data_move_redirect_content_guard_to_core.py +0 -0
  72. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0022_delete_contentredirectcontentguard.py +0 -0
  73. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0023_manifestsignature.py +0 -0
  74. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0024_containerremote_sigstore.py +0 -0
  75. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0025_signature_stored_in_textfield.py +0 -0
  76. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0026_manifest_signing_service.py +0 -0
  77. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0027_data_translate_perms_to_roles.py +0 -0
  78. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0028_add_role_manage_permissions.py +0 -0
  79. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0029_remove_blob_media_type.py +0 -0
  80. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0030_enforce_tagged_manifest_reference.py +0 -0
  81. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0031_replace_charf_with_textf.py +0 -0
  82. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0032_upload_artifact.py +0 -0
  83. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0033_raise_warning_for_repair.py +0 -0
  84. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0034_translate_signed_schema.py +0 -0
  85. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0035_alter_blob_content_ptr_and_more.py +0 -0
  86. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0036_containerpushrepository_pending_blobs_manifests.py +0 -0
  87. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0037_create_pull_through_cache_models.py +0 -0
  88. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0038_add_manifest_metadata_fields.py +0 -0
  89. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0039_manifest_data.py +0 -0
  90. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0040_add_remote_repo_filter.py +0 -0
  91. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0041_add_pull_through_pull_permissions.py +0 -0
  92. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0042_add_manifest_nature_field.py +0 -0
  93. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0043_add_os_arch_image_size_manifest_fields.py +0 -0
  94. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0044_add_domain.py +0 -0
  95. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0045_alter_manifest_compressed_image_size.py +0 -0
  96. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0046_alter_manifest_listed_manifests.py +0 -0
  97. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0047_containernamespace_pulp_labels.py +0 -0
  98. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0048_containerremote_includes_excludes.py +0 -0
  99. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0049_manifestsignature_fingerprint.py +0 -0
  100. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/0050_alter_containernamespace_options.py +0 -0
  101. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/migrations/__init__.py +0 -0
  102. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/modelresource.py +0 -0
  103. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/redirects.py +0 -0
  104. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/registry.py +0 -0
  105. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/replica.py +0 -0
  106. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/settings.py +0 -0
  107. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/__init__.py +0 -0
  108. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/builder.py +0 -0
  109. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/download_image_data.py +0 -0
  110. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/migrate_push_repository.py +0 -0
  111. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/recursive_add.py +0 -0
  112. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/recursive_remove.py +0 -0
  113. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/sign.py +0 -0
  114. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/sync_stages.py +0 -0
  115. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/synchronize.py +0 -0
  116. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/tag.py +0 -0
  117. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/tasks/untag.py +0 -0
  118. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/token_verification.py +0 -0
  119. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/urls.py +0 -0
  120. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/utils.py +0 -0
  121. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/webserver_snippets/__init__.py +0 -0
  122. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/webserver_snippets/apache.conf +0 -0
  123. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/app/webserver_snippets/nginx.conf +0 -0
  124. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/__init__.py +0 -0
  125. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/__init__.py +0 -0
  126. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/__init__.py +0 -0
  127. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_build_images.py +0 -0
  128. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_cancel_blob_upload.py +0 -0
  129. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_content_cache.py +0 -0
  130. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_crud_distributions.py +0 -0
  131. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_crud_remotes.py +0 -0
  132. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_delete_blob.py +0 -0
  133. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_delete_manifest.py +0 -0
  134. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_flatpak.py +0 -0
  135. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_migrate_push_repository.py +0 -0
  136. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_pull_content.py +0 -0
  137. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_pulpimportexport.py +0 -0
  138. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_rbac_push_repositories.py +0 -0
  139. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_rbac_remotes.py +0 -0
  140. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_rbac_repo_content.py +0 -0
  141. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_rbac_repo_versions.py +0 -0
  142. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_rbac_sync_repositories.py +0 -0
  143. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_recursive_add.py +0 -0
  144. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_recursive_remove.py +0 -0
  145. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_repositories_list.py +0 -0
  146. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_sync.py +0 -0
  147. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_sync_signatures.py +0 -0
  148. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/api/test_token_authentication.py +0 -0
  149. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/constants.py +0 -0
  150. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/functional/utils.py +0 -0
  151. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/unit/__init__.py +0 -0
  152. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/unit/fixtures/ryuk_0.13.0_manifest_list.json +0 -0
  153. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/unit/test_json_schemas.py +0 -0
  154. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/unit/test_models.py +0 -0
  155. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container/tests/unit/test_sync_stages.py +0 -0
  156. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container.egg-info/dependency_links.txt +0 -0
  157. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container.egg-info/entry_points.txt +0 -0
  158. {pulp_container-2.29.0 → pulp_container-2.29.2}/pulp_container.egg-info/top_level.txt +0 -0
  159. {pulp_container-2.29.0 → pulp_container-2.29.2}/setup.cfg +0 -0
  160. {pulp_container-2.29.0 → pulp_container-2.29.2}/test_requirements.txt +0 -0
  161. {pulp_container-2.29.0 → pulp_container-2.29.2}/unittest_requirements.txt +0 -0
@@ -8,6 +8,23 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 2.29.2 (2026-10-08) {: #2.29.2 }
12
+
13
+ #### Bugfixes {: #2.29.2-bugfix }
14
+
15
+ - Fixed global auth token leakage. CVE-2026-103868
16
+
17
+ ---
18
+
19
+ ## 2.29.1 (2026-09-23) {: #2.29.1 }
20
+
21
+ #### Bugfixes {: #2.29.1-bugfix }
22
+
23
+ - 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.
24
+ [#2494](https://github.com/pulp/pulp_container/issues/2494)
25
+
26
+ ---
27
+
11
28
  ## 2.29.0 (2026-08-10) {: #2.29.0 }
12
29
 
13
30
  #### Features {: #2.29.0-feature }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-container
3
- Version: 2.29.0
3
+ Version: 2.29.2
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
@@ -18,7 +18,7 @@ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: jsonschema<4.27,>=4.4
21
- Requires-Dist: pulpcore<3.130,>=3.111.0
22
- Requires-Dist: pyjwt[crypto]<2.14,>=2.4
23
- Requires-Dist: pysequoia<0.2.0,>=0.1.33
21
+ Requires-Dist: pulpcore<3.130,>=3.118.0
22
+ Requires-Dist: pyjwt[crypto]<2.16,>=2.4
23
+ Requires-Dist: pysequoia<0.2.0,>=0.1.35
24
24
  Dynamic: license-file
@@ -1,6 +1,5 @@
1
1
  pytest<10
2
- python-gnupg
3
2
  pytest-xdist
4
3
  pytest-timeout
5
4
  pytest-custom_exit_code
6
- trustme~=1.2.1
5
+ trustme~=1.2.1
@@ -24,7 +24,7 @@ class PulpContainerPluginAppConfig(PulpPluginAppConfig):
24
24
 
25
25
  name = "pulp_container.app"
26
26
  label = "container"
27
- version = "2.29.0"
27
+ version = "2.29.2"
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,8 +20,8 @@ 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
26
  TOKEN_EXPIRATION_TIME = getattr(settings, "TOKEN_EXPIRATION_TIME", 300)
28
27
 
@@ -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"
@@ -37,9 +40,9 @@ class RegistryContentCache(RegistryCache, AsyncContentCache):
37
40
  key = ":".join(all_keys[k] for k in self.keys)
38
41
  return key
39
42
 
40
- async def make_response(self, key, base_key):
43
+ async def make_response(self, key, base_key, *args, **kwargs):
41
44
  """Fix the Content-Type header to remove the charset."""
42
- response = await super().make_response(key, base_key)
45
+ response = await super().make_response(key, base_key, *args, **kwargs)
43
46
  if response is not None:
44
47
  if content_type := response.headers.get("Content-Type"):
45
48
  response.headers["Content-Type"] = content_type.split(";")[0]
@@ -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
+ ]
@@ -25,15 +25,12 @@ class RegistryAuthHttpDownloader(HttpDownloader):
25
25
  Additionally, use custom headers from DeclarativeArtifact.extra_data['headers']
26
26
  """
27
27
 
28
- registry_auth = {"bearer": None, "basic": None}
29
- token_lock = asyncio.Lock()
30
-
31
28
  def __init__(self, *args, **kwargs):
32
29
  """
33
30
  Initialize the downloader.
34
31
  """
35
32
  self.remote = kwargs.pop("remote")
36
-
33
+ self.factory = kwargs.pop("factory")
37
34
  super().__init__(*args, **kwargs)
38
35
 
39
36
  async def _run(self, handle_401=True, extra_data=None):
@@ -57,8 +54,8 @@ class RegistryAuthHttpDownloader(HttpDownloader):
57
54
  headers = extra_data.get("headers", headers)
58
55
  repo_name = extra_data.get("repo_name", None)
59
56
  http_method = extra_data.get("http_method", "get") if extra_data is not None else "get"
60
- this_token = self.registry_auth["bearer"]
61
- basic_auth = self.registry_auth["basic"]
57
+ this_token = self.factory.registry_auth["bearer"]
58
+ basic_auth = self.factory.registry_auth["basic"]
62
59
  auth_headers = self.auth_header(this_token, basic_auth)
63
60
  headers.update(auth_headers)
64
61
  # aiohttps does not allow to send auth argument and auth header together
@@ -81,16 +78,16 @@ class RegistryAuthHttpDownloader(HttpDownloader):
81
78
  if "bearer" in response_auth_header.lower():
82
79
  # Token has not been updated during request
83
80
  if (
84
- self.registry_auth["bearer"] is None
85
- or self.registry_auth["bearer"] == this_token
81
+ self.factory.registry_auth["bearer"] is None
82
+ or self.factory.registry_auth["bearer"] == this_token
86
83
  ):
87
- self.registry_auth["bearer"] = None
84
+ self.factory.registry_auth["bearer"] = None
88
85
  await self.update_token(response_auth_header, this_token, repo_name)
89
86
  return await self._run(handle_401=False, extra_data=extra_data)
90
87
  elif "basic" in response_auth_header.lower():
91
88
  if self.remote.username:
92
89
  basic = aiohttp.BasicAuth(self.remote.username, self.remote.password)
93
- self.registry_auth["basic"] = basic.encode()
90
+ self.factory.registry_auth["basic"] = basic.encode()
94
91
  return await self._run(handle_401=False, extra_data=extra_data)
95
92
  else:
96
93
  raise
@@ -111,10 +108,10 @@ class RegistryAuthHttpDownloader(HttpDownloader):
111
108
  """
112
109
  Update the Bearer token to be used with all requests.
113
110
  """
114
- async with self.token_lock:
111
+ async with self.factory.token_lock:
115
112
  if (
116
- self.registry_auth["bearer"] is not None
117
- and self.registry_auth["bearer"] == used_token
113
+ self.factory.registry_auth["bearer"] is not None
114
+ and self.factory.registry_auth["bearer"] == used_token
118
115
  ):
119
116
  return
120
117
  log.info("Updating bearer token")
@@ -157,7 +154,7 @@ class RegistryAuthHttpDownloader(HttpDownloader):
157
154
 
158
155
  token_js = json.loads(token_data)
159
156
  token = token_js.get("token") or token_js.get("access_token")
160
- self.registry_auth["bearer"] = token
157
+ self.factory.registry_auth["bearer"] = token
161
158
 
162
159
  @staticmethod
163
160
  def auth_header(token, basic_auth):
@@ -187,6 +184,21 @@ class RegistryAuthHttpDownloader(HttpDownloader):
187
184
  )
188
185
 
189
186
 
187
+ class RegistryAuthDownloaderFactory(DownloaderFactory):
188
+ """
189
+ A downloader factory for registry access that can also share the authentication tokens.
190
+ """
191
+
192
+ def __init__(self, *args, **kwargs):
193
+ super().__init__(*args, **kwargs)
194
+ self.registry_auth = {"bearer": None, "basic": None}
195
+ self.token_lock = asyncio.Lock()
196
+
197
+ def _http_or_https(self, download_class, url, **kwargs):
198
+ kwargs.setdefault("factory", self)
199
+ return super()._http_or_https(download_class, url, **kwargs)
200
+
201
+
190
202
  class NoAuthSignatureDownloader(HttpDownloader):
191
203
  """A downloader class suited for signature downloads."""
192
204
 
@@ -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
+ )
@@ -13,7 +13,6 @@ from django.shortcuts import redirect
13
13
  from django_lifecycle import AFTER_CREATE, AFTER_DELETE, AFTER_UPDATE, hook
14
14
 
15
15
  from pulpcore.plugin.cache import SyncContentCache
16
- from pulpcore.plugin.download import DownloaderFactory
17
16
  from pulpcore.plugin.models import (
18
17
  Artifact,
19
18
  AutoAddObjPermsMixin,
@@ -510,17 +509,17 @@ class ContainerRemote(Remote, AutoAddObjPermsMixin):
510
509
  """
511
510
  Downloader Factory that maps to custom downloaders which support registry auth.
512
511
 
513
- Upon first access, the DownloaderFactory is instantiated and saved internally.
512
+ Upon first access, the RegistryAuthDownloaderFactory is instantiated and saved internally.
514
513
 
515
514
  Returns:
516
- DownloadFactory: The instantiated DownloaderFactory to be used by
515
+ DownloadFactory: The instantiated RegistryAuthDownloaderFactory to be used by
517
516
  get_downloader()
518
517
 
519
518
  """
520
519
  try:
521
520
  return self._download_factory
522
521
  except AttributeError:
523
- self._download_factory = DownloaderFactory(
522
+ self._download_factory = downloaders.RegistryAuthDownloaderFactory(
524
523
  self,
525
524
  downloader_overrides={
526
525
  "http": downloaders.RegistryAuthHttpDownloader,
@@ -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
@@ -75,6 +75,10 @@ from pulp_container.app.exceptions import (
75
75
  RepositoryInvalid,
76
76
  RepositoryNotFound,
77
77
  )
78
+ from pulp_container.app.pull_through import (
79
+ get_pull_through_distribution,
80
+ get_pull_through_distribution_path,
81
+ )
78
82
  from pulp_container.app.redirects import (
79
83
  AzureStorageRedirects,
80
84
  FileStorageRedirects,
@@ -366,18 +370,12 @@ class ContainerRegistryApiMixin:
366
370
 
367
371
  def get_pull_through_drv(self, path):
368
372
  domain = get_domain()
369
- pull_through_cache_distribution = (
370
- models.ContainerPullThroughDistribution.objects.annotate(path=Value(path))
371
- .filter(path__startswith=F("base_path"), pulp_domain=domain)
372
- .order_by("-base_path")
373
- .first()
374
- )
373
+ pull_through_cache_distribution = get_pull_through_distribution(path, domain)
375
374
  if not pull_through_cache_distribution or not self.request.user.is_authenticated:
376
375
  raise RepositoryNotFound(name=path)
377
376
 
378
- upstream_name = path.split(pull_through_cache_distribution.base_path, maxsplit=1)[1].strip(
379
- "/"
380
- )
377
+ pull_through_path = get_pull_through_distribution_path(pull_through_cache_distribution)
378
+ upstream_name = path.split(pull_through_path, maxsplit=1)[1].strip("/")
381
379
  try:
382
380
  pull_through_remote = models.ContainerPullThroughRemote.objects.get(
383
381
  pk=pull_through_cache_distribution.remote_id
@@ -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
@@ -33,13 +34,31 @@ from pulpcore.plugin.util import get_domain
33
34
 
34
35
  from pulp_container.app import fields, models
35
36
  from pulp_container.app.utils import get_full_path
36
- from pulp_container.constants import SIGNATURE_TYPE
37
+ from pulp_container.constants import (
38
+ PULL_THROUGH_DISTRIBUTION_LABEL,
39
+ SIGNATURE_TYPE,
40
+ )
37
41
 
38
42
  VALID_SIGNATURE_NAME_REGEX = r"^sha256:[0-9a-f]{64}@[0-9a-f]{32}$"
39
43
  VALID_TAG_REGEX = r"^[A-Za-z0-9][A-Za-z0-9._-]*$"
40
44
  VALID_BASE_PATH_REGEX_COMPILED = re.compile(r"^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9])*$")
41
45
 
42
46
 
47
+ def validate_registry_path(value):
48
+ """Validate a path that will be exposed by the container registry."""
49
+ if len(value) > 255:
50
+ raise serializers.ValidationError(
51
+ _("The entered registry path cannot be longer than 255 characters.")
52
+ )
53
+
54
+ if not all(re.match(VALID_BASE_PATH_REGEX_COMPILED, part) for part in value.split("/")):
55
+ raise serializers.ValidationError(
56
+ _("The provided registry path contains forbidden characters.")
57
+ )
58
+
59
+ return value
60
+
61
+
43
62
  class TagSerializer(NoArtifactContentSerializer):
44
63
  """
45
64
  Serializer for Tags.
@@ -499,17 +518,9 @@ class ContainerDistributionSerializer(DistributionSerializer, GetOrCreateSeriali
499
518
 
500
519
  def validate_base_path(self, value):
501
520
  """Check whether the passed repository base path is valid or not."""
502
- if len(value) > 255:
503
- raise serializers.ValidationError(
504
- _("The entered base path cannot be longer than 255 characters.")
505
- )
506
-
507
- if not all(re.match(VALID_BASE_PATH_REGEX_COMPILED, p) for p in value.split("/")):
508
- raise serializers.ValidationError(
509
- _("The provided base path contains forbidden characters.")
510
- )
511
-
512
- return value
521
+ validate_registry_path(value)
522
+ # Ensure we follow pulpcore's cross-distribution overlap validation in addition to the OCI rules.
523
+ return super().validate_base_path(value)
513
524
 
514
525
  class Meta:
515
526
  model = models.ContainerDistribution
@@ -556,18 +567,49 @@ class ContainerPullThroughDistributionSerializer(DistributionSerializer):
556
567
  description = serializers.CharField(
557
568
  help_text=_("An optional description."), required=False, allow_null=True
558
569
  )
570
+ base_path = serializers.CharField(
571
+ help_text=_("The registry path exposed by this pull-through distribution.")
572
+ )
573
+
574
+ def validate_base_path(self, value):
575
+ """Check whether the passed repository base path is valid or not."""
576
+ validate_registry_path(value)
577
+ # Ensure we follow pulpcore's cross-distribution overlap validation in addition to the OCI rules.
578
+ return super().validate_base_path(value)
559
579
 
560
580
  def validate(self, data):
561
581
  validated_data = super().validate(data)
582
+ registry_path = validated_data.get("base_path")
583
+
584
+ # This is the temporary fix for pull-through distros violating core's overlapping base-path rule
585
+ # Save the base_path in pulp_labels to avoid overlapping base-paths with this PT's child distros
586
+ # A permanent fix should have us move off this field or Distributions all together.
587
+ if self.instance is None:
588
+ base_path = validated_data.pop("base_path")
589
+ validated_data["base_path"] = str(uuid4())
590
+ validated_data.setdefault("pulp_labels", {})[PULL_THROUGH_DISTRIBUTION_LABEL] = (
591
+ base_path
592
+ )
593
+ elif (
594
+ "base_path" in validated_data and validated_data["base_path"] != self.instance.base_path
595
+ ):
596
+ is_marked = (self.instance.pulp_labels or {}).get(PULL_THROUGH_DISTRIBUTION_LABEL)
597
+ if is_marked:
598
+ message = _("This value cannot be updated.")
599
+ else:
600
+ message = _(
601
+ "Run 'pulpcore-manager container-repair-pull-through-distributions' before "
602
+ "updating this value."
603
+ )
604
+ raise serializers.ValidationError({"base_path": message})
562
605
 
563
606
  if "content_guard" not in validated_data:
564
607
  validated_data["content_guard"] = ContentRedirectContentGuardSerializer.get_or_create(
565
608
  {"name": "content redirect", "pulp_domain": get_domain()}
566
609
  )
567
610
 
568
- base_path = validated_data.get("base_path")
569
- if base_path:
570
- namespace_name = base_path.split("/")[0]
611
+ if registry_path:
612
+ namespace_name = registry_path.split("/")[0]
571
613
  validated_data["namespace"] = ContainerNamespaceSerializer.get_or_create(
572
614
  {"name": namespace_name, "pulp_domain": get_domain()}
573
615
  )
@@ -11,6 +11,7 @@ from django_filters import CharFilter, MultipleChoiceFilter
11
11
  from drf_spectacular.utils import extend_schema
12
12
  from rest_framework import mixins
13
13
  from rest_framework.decorators import action
14
+ from rest_framework.exceptions import ValidationError
14
15
 
15
16
  from pulpcore.plugin.models import Content, PulpTemporaryFile
16
17
  from pulpcore.plugin.serializers import AsyncOperationResponseSerializer
@@ -40,6 +41,7 @@ from pulpcore.plugin.viewsets import (
40
41
  )
41
42
 
42
43
  from pulp_container.app import models, serializers, tasks
44
+ from pulp_container.constants import PULL_THROUGH_DISTRIBUTION_LABEL
43
45
 
44
46
  log = logging.getLogger(__name__)
45
47
 
@@ -1599,6 +1601,28 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin):
1599
1601
  ],
1600
1602
  }
1601
1603
 
1604
+ @action(
1605
+ detail=True,
1606
+ methods=["post"],
1607
+ serializer_class=DistributionViewSet.set_label.kwargs["serializer_class"],
1608
+ )
1609
+ def set_label(self, request, pk=None, **kwargs):
1610
+ """Prevent clients from changing the internal pull-through marker."""
1611
+ if request.data.get("key") == PULL_THROUGH_DISTRIBUTION_LABEL:
1612
+ raise ValidationError("This label is managed by pulp_container.")
1613
+ return super().set_label(request, pk, **kwargs)
1614
+
1615
+ @action(
1616
+ detail=True,
1617
+ methods=["post"],
1618
+ serializer_class=DistributionViewSet.unset_label.kwargs["serializer_class"],
1619
+ )
1620
+ def unset_label(self, request, pk=None, **kwargs):
1621
+ """Prevent clients from removing the internal pull-through marker."""
1622
+ if request.data.get("key") == PULL_THROUGH_DISTRIBUTION_LABEL:
1623
+ raise ValidationError("This label is managed by pulp_container.")
1624
+ return super().unset_label(request, pk, **kwargs)
1625
+
1602
1626
 
1603
1627
  class ContainerNamespaceViewSet(
1604
1628
  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",
@@ -237,11 +237,12 @@ def test_cross_domain_pulp_apis(
237
237
  },
238
238
  pulp_domain=domain1.name,
239
239
  )
240
+ pt_distribution_name = str(uuid.uuid4())
240
241
  with pytest.raises(container_bindings.ApiException) as e:
241
242
  container_bindings.DistributionsPullThroughApi.create(
242
243
  {
243
- "name": str(uuid.uuid4()),
244
- "base_path": str(uuid.uuid4()),
244
+ "name": pt_distribution_name,
245
+ "base_path": pt_distribution_name,
245
246
  "remote": pt_remote.pulp_href,
246
247
  },
247
248
  pulp_domain=domain2.name,