diff --git a/CHANGES/1497.feature b/CHANGES/1497.feature new file mode 100644 index 000000000..e016b03b8 --- /dev/null +++ b/CHANGES/1497.feature @@ -0,0 +1 @@ +Added configurable filtering of custom package metadata during sync and structured publish diff --git a/docs/user/guides/publish.md b/docs/user/guides/publish.md index e583ac8f9..0081961e6 100644 --- a/docs/user/guides/publish.md +++ b/docs/user/guides/publish.md @@ -63,6 +63,34 @@ It is also possible to use the `signing_service_release_overrides` parameter to This parameter expects a dict of key-value pairs of the form `{"": ""}`. At the time of this writing, this cannot be done via Pulp CLI. +## Filtering Package Metadata + +You can omit selected custom package metadata fields from generated `Packages` indices for a single publication or as a repository default. + +For example, to avoid publishing Ubuntu phased update metadata: + +```bash +pulp deb publication create \ + --repository=${NAME} \ + --excluded-package-metadata-field=Phased-Update-Percentage +``` + +To configure the same exclusion as a repository default: + +```bash +pulp deb repository update \ + --name=${NAME} \ + --excluded-package-metadata-field=Phased-Update-Percentage +``` + +The option may be specified multiple times. +This publish filter is useful even when existing package content already contains the custom fields. +The filter applies only to structured publications. Verbatim publications continue to serve upstream metadata unchanged. +Core package fields are never filtered, and matching is case-insensitive. + +Administrators can define additional global exclusions with the `EXCLUDED_PACKAGE_METADATA_FIELDS` setting. +Global, repository, and publication exclusions are additive. + ## Verbatim Publications !!! attention diff --git a/docs/user/guides/sync.md b/docs/user/guides/sync.md index 720eaf29e..8745fba8e 100644 --- a/docs/user/guides/sync.md +++ b/docs/user/guides/sync.md @@ -136,6 +136,27 @@ We recommend sticking to the following best practice recommendations: - Use a naming scheme for your remotes, that reflects the above. For example `nginx-bookworm-amd64` is a good name using the structure `--`. +## Filtering Package Metadata During Sync + +APT package indices may contain custom fields that are useful upstream but undesirable in a synced repository. +For example, Ubuntu repositories may include `Phased-Update-Percentage`, which describes upstream rollout policy rather than package identity. + +Use `--excluded-package-metadata-field` on an APT remote to avoid storing selected custom package metadata fields while syncing: + +```bash +pulp deb remote update \ + --name=${NAME} \ + --excluded-package-metadata-field=Phased-Update-Percentage +``` + +The option may be specified multiple times. +Only custom package metadata fields are filtered. +Core fields such as `Package`, `Version`, `Architecture`, `Filename`, checksums, and `Size` are never filtered. +Field matching is case-insensitive. + +Administrators can define additional global exclusions with the `EXCLUDED_PACKAGE_METADATA_FIELDS` setting. +Global and remote exclusions are additive. + ## Flat Repository Format Example `pulp_deb` supports synchronization from repositories using the deprecated [flat repository format](https://wiki.debian.org/DebianRepository/Format#Flat_Repository_Format). diff --git a/pulp_deb/app/metadata.py b/pulp_deb/app/metadata.py new file mode 100644 index 000000000..7ccfd70c0 --- /dev/null +++ b/pulp_deb/app/metadata.py @@ -0,0 +1,20 @@ +from django.conf import settings + + +def normalize_metadata_field_names(field_names): + return {field_name.lower() for field_name in (field_names or []) if field_name} + + +def excluded_package_metadata_fields(*field_name_lists): + """ + Return globally and locally configured excluded package metadata fields. + + Field matching is case insensitive. Local lists are additive with the global + ``EXCLUDED_PACKAGE_METADATA_FIELDS`` setting. + """ + excluded = normalize_metadata_field_names( + getattr(settings, "EXCLUDED_PACKAGE_METADATA_FIELDS", []) + ) + for field_names in field_name_lists: + excluded.update(normalize_metadata_field_names(field_names)) + return excluded diff --git a/pulp_deb/app/migrations/0042_add_excluded_package_metadata_fields.py b/pulp_deb/app/migrations/0042_add_excluded_package_metadata_fields.py new file mode 100644 index 000000000..46727e4ad --- /dev/null +++ b/pulp_deb/app/migrations/0042_add_excluded_package_metadata_fields.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.12 on 2026-07-07 07:48 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("deb", "0041_package_metadata_sha256"), + ] + + operations = [ + migrations.AddField( + model_name="aptpublication", + name="excluded_package_metadata_fields", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), default=list, size=None + ), + ), + migrations.AddField( + model_name="aptremote", + name="excluded_package_metadata_fields", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), default=list, size=None + ), + ), + migrations.AddField( + model_name="aptrepository", + name="excluded_package_metadata_fields", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), default=list, size=None + ), + ), + ] diff --git a/pulp_deb/app/models/publication.py b/pulp_deb/app/models/publication.py index b9bf2ac17..d02495a8a 100644 --- a/pulp_deb/app/models/publication.py +++ b/pulp_deb/app/models/publication.py @@ -1,6 +1,7 @@ from contextlib import suppress from datetime import timedelta +from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils import timezone from django_lifecycle import AFTER_CREATE, AFTER_UPDATE, hook @@ -72,6 +73,7 @@ class AptPublication(Publication, AutoAddObjPermsMixin): simple = models.BooleanField(default=False) structured = models.BooleanField(default=True) layout = models.TextField(choices=LAYOUT_CHOICES, default=LAYOUT_TYPES.NESTED_ALPHABETICALLY) + excluded_package_metadata_fields = ArrayField(models.TextField(), default=list) signing_service = models.ForeignKey( AptReleaseSigningService, on_delete=models.PROTECT, null=True ) diff --git a/pulp_deb/app/models/remote.py b/pulp_deb/app/models/remote.py index 82288c741..cbc439db0 100644 --- a/pulp_deb/app/models/remote.py +++ b/pulp_deb/app/models/remote.py @@ -1,3 +1,4 @@ +from django.contrib.postgres.fields import ArrayField from django.db import models from pulpcore.plugin.models import AutoAddObjPermsMixin, Remote @@ -18,6 +19,7 @@ class AptRemote(Remote, AutoAddObjPermsMixin): sync_installer = models.BooleanField(default=False) gpgkey = models.TextField(null=True) ignore_missing_package_indices = models.BooleanField(default=False) + excluded_package_metadata_fields = ArrayField(models.TextField(), default=list) class Meta: default_related_name = "%(app_label)s_%(model_name)s" diff --git a/pulp_deb/app/models/repository.py b/pulp_deb/app/models/repository.py index 4e193e889..6df8130b1 100644 --- a/pulp_deb/app/models/repository.py +++ b/pulp_deb/app/models/repository.py @@ -1,6 +1,7 @@ import logging from gettext import gettext as _ +from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.functional import cached_property @@ -66,6 +67,7 @@ class AptRepository(Repository, AutoAddObjPermsMixin): ] publish_upstream_release_fields = models.BooleanField(default=True) + excluded_package_metadata_fields = ArrayField(models.TextField(), default=list) signing_service = models.ForeignKey( AptReleaseSigningService, on_delete=models.PROTECT, null=True @@ -102,6 +104,7 @@ def on_new_version(self, version): simple=False, structured=True, signing_service_pk=getattr(self.signing_service, "pk", None), + excluded_package_metadata_fields=self.excluded_package_metadata_fields, ) class Meta: diff --git a/pulp_deb/app/serializers/content_serializers.py b/pulp_deb/app/serializers/content_serializers.py index 0f69c4a82..3a8b763fc 100644 --- a/pulp_deb/app/serializers/content_serializers.py +++ b/pulp_deb/app/serializers/content_serializers.py @@ -32,6 +32,9 @@ PACKAGE_UPLOAD_DEFAULT_COMPONENT, PACKAGE_UPLOAD_DEFAULT_DISTRIBUTION, ) +from pulp_deb.app.metadata import ( + excluded_package_metadata_fields as get_excluded_package_metadata_fields, +) from pulp_deb.app.models import ( BOOL_CHOICES, BasePackage, @@ -393,13 +396,16 @@ def from822(cls, data, **kwargs): Translate deb822.Package to a dictionary for class instatiation. """ skip = ["Filename", "MD5sum", "Size", "SHA1", "SHA256", "SHA512"] + excluded_fields = get_excluded_package_metadata_fields( + kwargs.pop("excluded_package_metadata_fields", None) + ) package_fields = {} custom_fields = {} for k, v in data.items(): if k in cls.TRANSLATION_DICT_INV: key = cls.TRANSLATION_DICT_INV[k] package_fields[key] = v - elif k not in skip: + elif k not in skip and k.lower() not in excluded_fields: # also save the fields not in TRANSLATION_DICT custom_fields[k] = v @@ -487,6 +493,7 @@ def to822( remote_artifact_dict=None, layout=LAYOUT_TYPES.NESTED_ALPHABETICALLY, basename_override=None, + excluded_package_metadata_fields=None, ): """Create deb822.Package object from model.""" ret = deb822.Packages() @@ -498,7 +505,14 @@ def to822( custom_fields = self.data.get("custom_fields") if custom_fields: - ret.update(custom_fields) + excluded_fields = get_excluded_package_metadata_fields(excluded_package_metadata_fields) + ret.update( + { + field_name: value + for field_name, value in custom_fields.items() + if field_name.lower() not in excluded_fields + } + ) artifact = None if artifact_dict and self.instance.sha256 in artifact_dict: diff --git a/pulp_deb/app/serializers/publication_serializers.py b/pulp_deb/app/serializers/publication_serializers.py index 81940f2ec..60aec8184 100644 --- a/pulp_deb/app/serializers/publication_serializers.py +++ b/pulp_deb/app/serializers/publication_serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from rest_framework.serializers import BooleanField, ValidationError +from rest_framework.serializers import BooleanField, CharField, ListField, ValidationError from pulpcore.plugin.models import Publication from pulpcore.plugin.serializers import ( @@ -54,6 +54,16 @@ class AptPublicationSerializer(PublicationSerializer): view_name="signing-services-detail", required=False, ) + excluded_package_metadata_fields = ListField( + child=CharField(), + help_text=( + "Package metadata field names to omit from generated Packages indices. " + "This only affects custom package metadata fields during structured publication. " + "Core package metadata fields are never filtered. Matching is case-insensitive. " + "Example: ['Phased-Update-Percentage']." + ), + required=False, + ) publish_legacy_release_files = BooleanField( help_text="Whether or not to publish Legacy per-component-and-architecture Release files.", default=False, @@ -76,6 +86,7 @@ class Meta: "signing_service", "publish_upstream_release_fields", "publish_legacy_release_files", + "excluded_package_metadata_fields", "layout", ) model = AptPublication diff --git a/pulp_deb/app/serializers/remote_serializers.py b/pulp_deb/app/serializers/remote_serializers.py index 3e393ad0c..05dfb5206 100644 --- a/pulp_deb/app/serializers/remote_serializers.py +++ b/pulp_deb/app/serializers/remote_serializers.py @@ -1,6 +1,12 @@ import re -from rest_framework.serializers import BooleanField, CharField, ChoiceField, ValidationError +from rest_framework.serializers import ( + BooleanField, + CharField, + ChoiceField, + ListField, + ValidationError, +) from pulpcore.plugin.models import Remote from pulpcore.plugin.serializers import RemoteSerializer @@ -68,6 +74,17 @@ class AptRemoteSerializer(RemoteSerializer): required=False, ) + excluded_package_metadata_fields = ListField( + child=CharField(), + help_text=( + "Package metadata field names to drop while syncing package indices. " + "Only custom package metadata fields are affected. Core fields such as Package, " + "Version, Architecture, Filename, checksums, and Size are never filtered. " + "Matching is case-insensitive. Example: ['Phased-Update-Percentage']." + ), + required=False, + ) + policy = ChoiceField( help_text="The policy to use when downloading content. The possible values include: " "'immediate', 'on_demand', and 'streamed'. 'immediate' is the default.", @@ -85,6 +102,7 @@ class Meta: "sync_installer", "gpgkey", "ignore_missing_package_indices", + "excluded_package_metadata_fields", ) model = AptRemote diff --git a/pulp_deb/app/serializers/repository_serializers.py b/pulp_deb/app/serializers/repository_serializers.py index a6e4ef730..0e64fa00a 100644 --- a/pulp_deb/app/serializers/repository_serializers.py +++ b/pulp_deb/app/serializers/repository_serializers.py @@ -117,6 +117,19 @@ class AptRepositorySerializer(RepositorySerializer): required=False, allow_null=True, ) + + excluded_package_metadata_fields = serializers.ListField( + child=serializers.CharField(), + help_text=_( + "Default package metadata field names to omit from generated Packages indices. " + "This repository level setting is used when a publication does not specify its own " + "excluded_package_metadata_fields. Only custom package metadata fields are affected. " + "Core fields are never filtered. Matching is case-insensitive. " + "Example: ['Phased-Update-Percentage']." + ), + required=False, + ) + package_signing_fingerprint = PgpKeyFingerprintField( help_text=_( "The pubkey fingerprint to be passed to the package signing service. " @@ -138,6 +151,7 @@ class Meta: "package_signing_fingerprint_release_overrides", "package_signing_service", "package_signing_fingerprint", + "excluded_package_metadata_fields", ) model = AptRepository diff --git a/pulp_deb/app/settings.py b/pulp_deb/app/settings.py index 54db5da87..ae42d2c8b 100644 --- a/pulp_deb/app/settings.py +++ b/pulp_deb/app/settings.py @@ -14,3 +14,4 @@ STRUCTURED_EMPTY_REPO_COMPONENT = "empty" STRUCTURED_EMPTY_REPO_ARCHITECTURES = ["all"] MAX_PACKAGE_SIGNING_WORKERS = 5 +EXCLUDED_PACKAGE_METADATA_FIELDS = [] diff --git a/pulp_deb/app/tasks/publishing.py b/pulp_deb/app/tasks/publishing.py index 307905651..53a91d427 100644 --- a/pulp_deb/app/tasks/publishing.py +++ b/pulp_deb/app/tasks/publishing.py @@ -33,6 +33,9 @@ NO_MD5_WARNING_MESSAGE, NULL_VALUE, ) +from pulp_deb.app.metadata import ( + excluded_package_metadata_fields as normalize_package_metadata_fields, +) from pulp_deb.app.models import ( AptPublication, AptReleaseSigningService, @@ -85,6 +88,7 @@ def publish( publish_upstream_release_fields=None, layout=LAYOUT_TYPES.NESTED_ALPHABETICALLY, publish_legacy_release_files=False, + excluded_package_metadata_fields=None, ): """ Use provided publisher to create a Publication based on a RepositoryVersion. @@ -97,6 +101,8 @@ def publish( signing_service_pk (str): Use this SigningService to sign the Release files. layout (str): The layout determines the form the package urls take. publish_legacy_release_files (bool): publish legacy per architecture release files + excluded_package_metadata_fields (list): Custom package metadata fields to omit from + generated package indices. """ @@ -129,6 +135,11 @@ def publish( publication.publish_legacy_release_files = publish_legacy_release_files publication.layout = layout repository = AptRepository.objects.get(pk=repo_version.repository.pk) + publication.excluded_package_metadata_fields = ( + excluded_package_metadata_fields + if excluded_package_metadata_fields is not None + else repository.excluded_package_metadata_fields + ) if simple: release = Release( @@ -413,6 +424,9 @@ def add_packages(self, package_pairs, artifact_dict, remote_artifact_dict): package.pk: list(package.contentartifact_set.all()) for package in packages } layout = self.parent.publication.layout + excluded_fields = normalize_package_metadata_fields( + self.parent.publication.excluded_package_metadata_fields + ) seen_published_artifacts = set() seen_package_index_entries = set() @@ -468,6 +482,7 @@ def add_packages(self, package_pairs, artifact_dict, remote_artifact_dict): remote_artifact_dict, layout=layout, basename_override=upstream_basename, + excluded_package_metadata_fields=excluded_fields, ).dump(self.package_index_files[metadata_arch][0]) except KeyError: log.warning( diff --git a/pulp_deb/app/tasks/synchronizing.py b/pulp_deb/app/tasks/synchronizing.py index 9c5067931..5c06abf2f 100644 --- a/pulp_deb/app/tasks/synchronizing.py +++ b/pulp_deb/app/tasks/synchronizing.py @@ -976,7 +976,10 @@ async def _handle_package_index( package_class = InstallerPackage serializer_class = InstallerPackage822Serializer log.debug(_("Downloading package {}").format(package_paragraph["Package"])) - serializer = serializer_class.from822(data=package_paragraph) + serializer = serializer_class.from822( + data=package_paragraph, + excluded_package_metadata_fields=(self.remote.excluded_package_metadata_fields), + ) serializer.is_valid(raise_exception=True) package_metadata = serializer.validated_data package_content_unit = package_class( diff --git a/pulp_deb/app/viewsets/publication.py b/pulp_deb/app/viewsets/publication.py index d4a26208f..fe0fa9c50 100644 --- a/pulp_deb/app/viewsets/publication.py +++ b/pulp_deb/app/viewsets/publication.py @@ -218,6 +218,9 @@ def create(self, request, **kwargs): ) publish_legacy_release_files = serializer.validated_data.get("publish_legacy_release_files") layout = serializer.validated_data.get("layout") + excluded_package_metadata_fields = serializer.validated_data.get( + "excluded_package_metadata_fields" + ) kwargs = { "repository_version_pk": repository_version.pk, @@ -227,6 +230,7 @@ def create(self, request, **kwargs): "publish_upstream_release_fields": publish_upstream_release_fields, "publish_legacy_release_files": publish_legacy_release_files, "layout": layout, + "excluded_package_metadata_fields": excluded_package_metadata_fields, } if checkpoint: kwargs["checkpoint"] = True diff --git a/pulp_deb/tests/functional/api/test_publish.py b/pulp_deb/tests/functional/api/test_publish.py index 27ab683be..3aaf5093e 100644 --- a/pulp_deb/tests/functional/api/test_publish.py +++ b/pulp_deb/tests/functional/api/test_publish.py @@ -10,10 +10,13 @@ from pulp_deb.tests.functional.constants import ( DEB_FIXTURE_ALT_SINGLE_DIST, + DEB_FIXTURE_ARCH, DEB_FIXTURE_BASE, DEB_FIXTURE_COMPLEX_REPOSITORY_NAME, + DEB_FIXTURE_COMPONENT, DEB_FIXTURE_DISTRIBUTIONS, DEB_FIXTURE_FLAT_REPOSITORY_NAME, + DEB_FIXTURE_METADATA_UPDATE_REPOSITORY_NAME, DEB_FIXTURE_MISSING_ARCHITECTURE_REPOSITORY_NAME, DEB_FIXTURE_SINGLE_DIST, DEB_FIXTURE_VARIANT_REPOSITORY_NAME, @@ -146,6 +149,55 @@ def _verify_distribution(distribution, expected): return _verify_distribution +@pytest.mark.parallel +def test_publish_excludes_package_metadata_fields( + apt_distribution_api, + deb_init_and_sync, + deb_publication_factory, + deb_distribution_factory, + download_content_unit, +): + """Test that structured publish can omit selected custom package metadata fields.""" + remote_args = { + "distributions": DEB_FIXTURE_SINGLE_DIST, + "components": DEB_FIXTURE_COMPONENT, + "architectures": DEB_FIXTURE_ARCH, + } + repo, _ = deb_init_and_sync( + url=DEB_FIXTURE_METADATA_UPDATE_REPOSITORY_NAME, + remote_args=remote_args, + ) + + default_publication = deb_publication_factory(repo, simple=False, structured=True) + default_distribution = apt_distribution_api.read( + deb_distribution_factory(default_publication).pulp_href + ) + default_packages_index = download_content_unit( + default_distribution.to_dict()["base_path"], + "dists/ragnarok/asgard/binary-ppc64/Packages", + ).decode("utf-8") + + assert "Package: frigg" in default_packages_index + assert "Phased-Update-Percentage: 50" in default_packages_index + + filtered_publication = deb_publication_factory( + repo, + simple=False, + structured=True, + excluded_package_metadata_fields=["Phased-Update-Percentage"], + ) + filtered_distribution = apt_distribution_api.read( + deb_distribution_factory(filtered_publication).pulp_href + ) + filtered_packages_index = download_content_unit( + filtered_distribution.to_dict()["base_path"], + "dists/ragnarok/asgard/binary-ppc64/Packages", + ).decode("utf-8") + + assert "Package: frigg" in filtered_packages_index + assert "Phased-Update-Percentage" not in filtered_packages_index + + @pytest.mark.parallel @pytest.mark.parametrize( "publication_args", diff --git a/pulp_deb/tests/functional/api/test_sync.py b/pulp_deb/tests/functional/api/test_sync.py index 75eeb71fa..8e8540561 100644 --- a/pulp_deb/tests/functional/api/test_sync.py +++ b/pulp_deb/tests/functional/api/test_sync.py @@ -394,6 +394,42 @@ def test_sync_replaces_package_when_only_metadata_changes( assert historical_frigg[0].metadata_sha256 == v1_frigg.metadata_sha256 +@pytest.mark.parallel +def test_sync_excludes_package_metadata_fields( + apt_distribution_api, + deb_init_and_sync, + deb_publication_factory, + deb_distribution_factory, + download_content_unit, +): + """Exclude selected custom package metadata fields while syncing.""" + remote_args = { + "distributions": DEB_FIXTURE_SINGLE_DIST, + "components": DEB_FIXTURE_COMPONENT, + "architectures": DEB_FIXTURE_ARCH, + "excluded_package_metadata_fields": ["Phased-Update-Percentage"], + } + repo, _ = deb_init_and_sync( + url=DEB_FIXTURE_METADATA_UPDATE_REPOSITORY_NAME, + remote_args=remote_args, + ) + + publication = deb_publication_factory( + repo, + simple=False, + structured=True, + excluded_package_metadata_fields=[], + ) + distribution = apt_distribution_api.read(deb_distribution_factory(publication).pulp_href) + packages_index = download_content_unit( + distribution.to_dict()["base_path"], + "dists/ragnarok/asgard/binary-ppc64/Packages", + ).decode("utf-8") + + assert "Package: frigg" in packages_index + assert "Phased-Update-Percentage" not in packages_index + + @pytest.mark.parallel def test_sync_optimize_switch_to_no_mirror(deb_init_and_sync): """ diff --git a/pulp_deb/tests/unit/test_serializers.py b/pulp_deb/tests/unit/test_serializers.py index 99f1ce32d..6e824ec21 100644 --- a/pulp_deb/tests/unit/test_serializers.py +++ b/pulp_deb/tests/unit/test_serializers.py @@ -5,7 +5,7 @@ from pulpcore.plugin.models import Artifact -from pulp_deb.app.models import GenericContent +from pulp_deb.app.models import GenericContent, Package from pulp_deb.app.serializers import GenericContentSerializer from pulp_deb.app.serializers.content_serializers import Package822Serializer @@ -83,3 +83,45 @@ def test_package_822_serializer_metadata_digest_changes_with_custom_metadata(): assert calculate_package_metadata_sha256( old_serializer.validated_data ) != calculate_package_metadata_sha256(new_serializer.validated_data) + + +def test_package_822_serializer_excludes_custom_metadata_on_sync(): + """Filtered custom package metadata fields are not stored during sync.""" + paragraph = deb822.Packages() + paragraph["Package"] = "foo" + paragraph["Version"] = "1.0" + paragraph["Architecture"] = "amd64" + paragraph["Maintainer"] = "Example Maintainer " + paragraph["Description"] = "Example pacakge" + paragraph["Phased-Update-Percentage"] = "10" + paragraph["X-Keep-Me"] = "kept" + + serializer = Package822Serializer.from822( + paragraph, excluded_package_metadata_fields=["phased-update-percentage"] + ) + + assert serializer.is_valid(), serializer.errors + assert serializer.validated_data["custom_fields"] == {"X-Keep-Me": "kept"} + + +def test_package_822_serializer_excludes_custom_metadata_on_publish(): + """Filtered custom package metadata fields are not emitted during structured publish.""" + package = Package( + package="foo", + version="1.0", + architecture="amd64", + maintainer="Example Maintainer ", + description="Example package", + relative_path="pool/main/f/foo/foo_1.0_amd64.deb", + sha256="0123456789abcdef", + custom_fields={ + "Phased-Update-Percentage": "10", + "X-Keep-Me": "kept", + }, + ) + serializer = Package822Serializer(package, context={"request": None}) + + paragraph = serializer.to822(excluded_package_metadata_fields=["PHASED-UPDATE-PERCENTAGE"]) + + assert "Phased-Update-Percentage" not in paragraph + assert paragraph["X-Keep-Me"] == "kept"