From bba744ba12aa8b339f6799517e5c8cac3915be78 Mon Sep 17 00:00:00 2001 From: Quirin Pamp Date: Thu, 2 Apr 2026 15:05:06 +0200 Subject: [PATCH] Merge pull request #1424 from ATIX-AG/add-support-for-empty-repo-publish Allow publication of empty repositories (cherry picked from commit 482bd0d45442ffe5f7f4335e8714d4c3e7fd7ea6) --- CHANGES/+publish_empty_repos.feature | 1 + pulp_deb/app/settings.py | 4 + pulp_deb/app/tasks/publishing.py | 180 +++++++++++------- pulp_deb/tests/functional/api/test_publish.py | 83 ++++++++ 4 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 CHANGES/+publish_empty_repos.feature diff --git a/CHANGES/+publish_empty_repos.feature b/CHANGES/+publish_empty_repos.feature new file mode 100644 index 000000000..4ff9c0f8f --- /dev/null +++ b/CHANGES/+publish_empty_repos.feature @@ -0,0 +1 @@ +Provide minimal metadata files for empty repository versions during structured publish. diff --git a/pulp_deb/app/settings.py b/pulp_deb/app/settings.py index 7bed28931..80581f66c 100644 --- a/pulp_deb/app/settings.py +++ b/pulp_deb/app/settings.py @@ -9,3 +9,7 @@ FORBIDDEN_CHECKSUM_WARNINGS = True FORCE_IGNORE_MISSING_PACKAGE_INDICES = False PERMISSIVE_SYNC = False + +STRUCTURED_EMPTY_REPO_DISTRIBUTION = "default" +STRUCTURED_EMPTY_REPO_COMPONENT = "empty" +STRUCTURED_EMPTY_REPO_ARCHITECTURES = ["all"] diff --git a/pulp_deb/app/tasks/publishing.py b/pulp_deb/app/tasks/publishing.py index c9283db42..0e14a33e0 100644 --- a/pulp_deb/app/tasks/publishing.py +++ b/pulp_deb/app/tasks/publishing.py @@ -176,6 +176,7 @@ def publish( "distribution", flat=True ) ) + has_structured_distribution = bool(distributions) if simple and "default" in distributions: message = ( @@ -186,55 +187,23 @@ def publish( distributions.remove("default") release_helpers = [] - for distribution in distributions: - architectures = list( - ReleaseArchitecture.objects.filter( - pk__in=repo_version.content.order_by("-pulp_created"), - distribution=distribution, - ) - .distinct("architecture") - .values_list("architecture", flat=True) - ) + + if not has_structured_distribution and not simple: + distribution = settings.STRUCTURED_EMPTY_REPO_DISTRIBUTION + components = [settings.STRUCTURED_EMPTY_REPO_COMPONENT] + architectures = list(settings.STRUCTURED_EMPTY_REPO_ARCHITECTURES) if "all" not in architectures: architectures.append("all") - release = Release.objects.filter( - pk__in=repo_version.content.order_by("-pulp_created"), + codename = distribution.strip("/").split("/")[0] + release = Release( distribution=distribution, - ).first() - publish_upstream = ( - publish_upstream_release_fields - if publish_upstream_release_fields is not None - else repository.publish_upstream_release_fields - ) - if not release: - codename = distribution.strip("/").split("/")[0] - release = Release( - distribution=distribution, - codename=codename, - suite=codename, - origin="Pulp 3", - ) - if repository.description: - release.description = repository.description - elif not publish_upstream: - release = Release( - distribution=release.distribution, - codename=release.codename, - suite=release.suite, - origin="Pulp 3", - ) - if repository.description: - release.description = repository.description - - release_components_filtered = release_components.filter( - distribution=distribution - ) - components = list( - release_components_filtered.distinct("component").values_list( - "component", flat=True - ) + codename=codename, + suite=codename, + origin="Pulp 3", ) + if repository.description: + release.description = repository.description signing_service = repository.release_signing_service(release) @@ -246,43 +215,108 @@ def publish( temp_dir=temp_dir, signing_service=signing_service, ) + release_helper.save_unsigned_metadata() + release_helpers.append(release_helper) + else: + for distribution in distributions: + architectures = list( + ReleaseArchitecture.objects.filter( + pk__in=repo_version.content.order_by("-pulp_created"), + distribution=distribution, + ) + .distinct("architecture") + .values_list("architecture", flat=True) + ) + if "all" not in architectures: + architectures.append("all") - package_release_components = PackageReleaseComponent.objects.filter( - pk__in=repo_version.content.order_by("-pulp_created"), - release_component__in=release_components_filtered, - ).select_related("release_component", "package") + release = Release.objects.filter( + pk__in=repo_version.content.order_by("-pulp_created"), + distribution=distribution, + ).first() + publish_upstream = ( + publish_upstream_release_fields + if publish_upstream_release_fields is not None + else repository.publish_upstream_release_fields + ) + if not release: + codename = distribution.strip("/").split("/")[0] + release = Release( + distribution=distribution, + codename=codename, + suite=codename, + origin="Pulp 3", + ) + if repository.description: + release.description = repository.description + elif not publish_upstream: + release = Release( + distribution=release.distribution, + codename=release.codename, + suite=release.suite, + origin="Pulp 3", + ) + if repository.description: + release.description = repository.description + + release_components_filtered = release_components.filter( + distribution=distribution + ) + components = list( + release_components_filtered.distinct("component").values_list( + "component", flat=True + ) + ) + + signing_service = repository.release_signing_service(release) - source_package_release_components = ( - SourcePackageReleaseComponent.objects.filter( + release_helper = _ReleaseHelper( + publication=publication, + components=components, + architectures=architectures, + release=release, + temp_dir=temp_dir, + signing_service=signing_service, + ) + + package_release_components = PackageReleaseComponent.objects.filter( pk__in=repo_version.content.order_by("-pulp_created"), release_component__in=release_components_filtered, - ).select_related("release_component", "source_package") - ) + ).select_related("release_component", "package") - for component in components: - packages = Package.objects.filter( - pk__in=[ - prc.package.pk - for prc in package_release_components - if prc.release_component.component == component - ] - ).prefetch_related("contentartifact_set", "_artifacts") - artifact_dict, remote_artifact_dict = _batch_fetch_artifacts(packages) - release_helper.components[component].add_packages( - packages, - artifact_dict, - remote_artifact_dict, + source_package_release_components = ( + SourcePackageReleaseComponent.objects.filter( + pk__in=repo_version.content.order_by("-pulp_created"), + release_component__in=release_components_filtered, + ).select_related("release_component", "source_package") ) - source_packages = [ - drc.source_package - for drc in source_package_release_components - if drc.release_component.component == component - ] - release_helper.components[component].add_source_packages(source_packages) + for component in components: + packages = Package.objects.filter( + pk__in=[ + prc.package.pk + for prc in package_release_components + if prc.release_component.component == component + ] + ).prefetch_related("contentartifact_set", "_artifacts") + artifact_dict, remote_artifact_dict = _batch_fetch_artifacts(packages) + release_helper.components[component].add_packages( + packages, + artifact_dict, + remote_artifact_dict, + ) + + source_packages = [ + drc.source_package + for drc in source_package_release_components + if drc.release_component.component == component + ] + release_helper.components[component].add_source_packages( + source_packages + ) - release_helper.save_unsigned_metadata() - release_helpers.append(release_helper) + release_helper.save_unsigned_metadata() + release_helpers.append(release_helper) asyncio.run(_concurrently_sign_metadata(release_helpers)) for release_helper in release_helpers: diff --git a/pulp_deb/tests/functional/api/test_publish.py b/pulp_deb/tests/functional/api/test_publish.py index 8e00a2e5b..c6b34af83 100644 --- a/pulp_deb/tests/functional/api/test_publish.py +++ b/pulp_deb/tests/functional/api/test_publish.py @@ -3,6 +3,7 @@ import pytest from debian import deb822 +from django.conf import settings from pulpcore.client.pulp_deb.exceptions import ApiException @@ -594,6 +595,88 @@ def test_remove_all_content_from_repository( assert len(prcs) == 0 +@pytest.mark.parallel +def test_publish_truly_empty_repository_structured( + apt_distribution_api, + deb_distribution_factory, + deb_publication_factory, + deb_repository_factory, + download_content_unit, +): + """Test that a truly empty repository can be published in structured mode. + + The publication should synthesize a default distribution/component and publish + empty package metadata that apt can consume. + """ + repo = deb_repository_factory() + + publication = deb_publication_factory( + repo, + simple=False, + structured=True, + ) + + distribution = deb_distribution_factory(publication) + distribution = apt_distribution_api.read(distribution.pulp_href) + + base_path = distribution.to_dict()["base_path"] + + dist_path = "dists/" + settings.STRUCTURED_EMPTY_REPO_DISTRIBUTION + "/" + comp_path = dist_path + settings.STRUCTURED_EMPTY_REPO_COMPONENT + "/" + + release_path = dist_path + "Release" + packages_path = comp_path + "binary-all/Packages" + packages_gz_path = comp_path + "binary-all/Packages.gz" + + release = download_content_unit(base_path, release_path).decode("utf-8") + packages = download_content_unit(base_path, packages_path) + packages_gz = download_content_unit(base_path, packages_gz_path) + + assert "404: Not Found" not in release + assert packages is not None + assert packages_gz is not None + + assert "Codename: " + settings.STRUCTURED_EMPTY_REPO_DISTRIBUTION in release + assert "Suite: " + settings.STRUCTURED_EMPTY_REPO_DISTRIBUTION in release + assert "Components: " + settings.STRUCTURED_EMPTY_REPO_COMPONENT in release + assert "Architectures: all" in release + assert settings.STRUCTURED_EMPTY_REPO_COMPONENT + "/binary-all/Packages" in release + assert settings.STRUCTURED_EMPTY_REPO_COMPONENT + "/binary-all/Packages.gz" in release + + assert packages.decode("utf-8") == "" + + +@pytest.mark.parallel +def test_publish_truly_empty_repository_simple_only( + apt_distribution_api, + deb_distribution_factory, + deb_publication_factory, + deb_repository_factory, + download_content_unit, +): + """Test that simple mode keeps its existing empty-repo behavior.""" + repo = deb_repository_factory() + + publication = deb_publication_factory( + repo, + simple=True, + structured=False, + ) + + distribution = deb_distribution_factory(publication) + distribution = apt_distribution_api.read(distribution.pulp_href) + + base_path = distribution.to_dict()["base_path"] + + release = download_content_unit(base_path, "dists/default/Release").decode("utf-8") + packages = download_content_unit(base_path, "dists/default/all/binary-all/Packages") + + assert "Codename: default" in release + assert "Components: all" in release + assert "Architectures: all" in release + assert packages.decode("utf-8") == "" + + def assert_equal_package_index(orig, new): """In-detail check of two PackageIndex file-strings""" parsed_orig = parse_package_index(orig)