Skip to content

Commit 1a31fd5

Browse files
committed
Scope wildcard content removal
Define wildcard repository removal according to the supplied distribution and component selectors: - remove_content_units=['*'] with a specified distribution and component removes all content from that release component. - remove_content_units=['*'] with a specified distribution and component='*' removes all content from that distribution. - remove_content_units=['*'] with distribution='*' and component='*' removes every content unit from the repository. - remove_content_units=['*'] without either selector preserves the existing behavior and removes every content unit from the repository. Scoped wildcards are expanded through binary and source package component relationships in the base repository version. Content that remain linked outside the selected scope are preserved, while matching relationships are removed. Document the wildcard contract in the modify serializer and changelog, and add functional coverage for component-scoped, distribution-scoped, and complete repository removal. Assisted-by: GitHub Copilot
1 parent 5d7a05f commit 1a31fd5

4 files changed

Lines changed: 281 additions & 16 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added scoped wildcard removal for APT repositories. Using `remove_content_units` with `distribution` and `component` can now empty a single component or a whole distribution, removing the covered packages along with their `ReleaseComponent` and, for a whole distribution, its `Release` and `ReleaseArchitectures`. Setting both selectors to `*` removes all content units from the repository.

pulp_deb/app/serializers/repository_serializers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,21 @@ class AptRepositoryAddRemoveContentSerializer(RepositoryAddRemoveContentSerializ
3232
help_text=_(
3333
"Name of the distribution any packages from add_content_units or remove_content_units "
3434
"should be added to or removed from. Defaults to DEFAULT_DISTRIBUTION if only a "
35-
"component is provided."
35+
"component is provided. When remove_content_units is ['*'], a distribution limits "
36+
"the removal to packages in that distribution, along with its Release and "
37+
"ReleaseArchitectures. Set both distribution and component to '*' to remove all "
38+
"content units from the repository."
3639
),
3740
required=False,
3841
)
3942
component = serializers.CharField(
4043
help_text=_(
4144
"Name of the component any packages from add_content_units or remove_content_units "
4245
"should be added to or removed from. Defaults to DEFAULT_COMPONENT if only a "
43-
"distribution is provided.."
46+
"distribution is provided. When remove_content_units is ['*'], a component limits "
47+
"the removal to packages in that component, along with its ReleaseComponent. Set "
48+
"component to '*' to remove packages from every component in the selected "
49+
"distribution."
4450
),
4551
required=False,
4652
)

pulp_deb/app/tasks/signing.py

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
AptRepository,
3030
Package,
3131
PackageReleaseComponent,
32+
Release,
3233
ReleaseArchitecture,
3334
ReleaseComponent,
3435
SourcePackage,
@@ -42,16 +43,54 @@
4243
log = logging.getLogger(__name__)
4344

4445

46+
def _filter_by_scope(queryset, distribution, component, prefix=""):
47+
"""Narrow a queryset to a distribution/component, where "*" matches every value."""
48+
if distribution != "*":
49+
queryset = queryset.filter(**{f"{prefix}distribution": distribution})
50+
if component != "*":
51+
queryset = queryset.filter(**{f"{prefix}component": component})
52+
return queryset
53+
54+
55+
def _prepare_release_removals(repository_version, remove_content_units, distribution, component):
56+
"""Expand a scoped wildcard removal to the release metadata it covers.
57+
58+
Emptying a single component drops only that ReleaseComponent, while emptying a whole
59+
distribution ("*" component) also drops its Release and ReleaseArchitectures.
60+
"""
61+
release_components = _filter_by_scope(
62+
ReleaseComponent.objects.filter(pk__in=repository_version.content),
63+
distribution,
64+
component,
65+
)
66+
remove_content_units.extend(str(pk) for pk in release_components.values_list("pk", flat=True))
67+
if component != "*":
68+
return
69+
70+
for model in (Release, ReleaseArchitecture):
71+
units = model.objects.filter(pk__in=repository_version.content)
72+
if distribution != "*":
73+
units = units.filter(distribution=distribution)
74+
remove_content_units.extend(str(pk) for pk in units.values_list("pk", flat=True))
75+
76+
4577
def _prepare_package_removals(repo, remove_content_units, base_version_pk, distribution, component):
4678
"""Expand the removal list to include the release component relationships of each package.
4779
4880
Removing a (source) package also requires removing its PackageReleaseComponent /
4981
SourcePackageReleaseComponent links. When a distribution/component is given, the removal is
5082
scoped to that component: a package is only removed from the repository if the scope held its
5183
last relationship, so packages linked elsewhere or not linked at all are kept.
84+
85+
A "*" removal names every package in scope rather than an explicit list, and additionally
86+
removes the release metadata that scope covers.
5287
"""
53-
# "*" removes all content, so there is nothing to resolve here.
54-
if not remove_content_units or "*" in remove_content_units:
88+
if not remove_content_units:
89+
return
90+
91+
wildcard_removal = "*" in remove_content_units
92+
# An unscoped wildcard removes all repository content through pulpcore.
93+
if wildcard_removal and distribution in (None, "*") and component in (None, "*"):
5594
return
5695

5796
repository_version = (
@@ -64,23 +103,35 @@ def _prepare_package_removals(repo, remove_content_units, base_version_pk, distr
64103
if scoped:
65104
distribution = distribution or DEFAULT_DISTRIBUTION
66105
component = component or DEFAULT_COMPONENT
106+
if wildcard_removal:
107+
remove_content_units.clear()
67108

68109
for model, relationship_model, relationship_field in (
69110
(Package, PackageReleaseComponent, "package"),
70111
(SourcePackage, SourcePackageReleaseComponent, "source_package"),
71112
):
72-
units = model.objects.filter(pk__in=remove_content_units)
73-
relationships = relationship_model.objects.filter(
74-
**{
75-
f"{relationship_field}__in": units,
76-
"pk__in": repository_version.content,
77-
}
78-
)
79-
if scoped:
80-
scoped_relationships = relationships.filter(
81-
release_component__distribution=distribution,
82-
release_component__component=component,
113+
if wildcard_removal:
114+
relationships = relationship_model.objects.filter(pk__in=repository_version.content)
115+
scoped_relationships = _filter_by_scope(
116+
relationships, distribution, component, "release_component__"
117+
)
118+
units = model.objects.filter(
119+
pk__in=scoped_relationships.values_list(f"{relationship_field}_id", flat=True)
83120
)
121+
remove_content_units.extend(str(pk) for pk in units.values_list("pk", flat=True))
122+
else:
123+
units = model.objects.filter(pk__in=remove_content_units)
124+
relationships = relationship_model.objects.filter(
125+
**{
126+
f"{relationship_field}__in": units,
127+
"pk__in": repository_version.content,
128+
}
129+
)
130+
if scoped:
131+
scoped_relationships = _filter_by_scope(
132+
relationships, distribution, component, "release_component__"
133+
)
134+
if scoped:
84135
# Relationships named in the request are removed alongside the scoped ones.
85136
removed_relationship_ids = set(
86137
relationship_model.objects.filter(pk__in=remove_content_units).values_list(
@@ -102,6 +153,9 @@ def _prepare_package_removals(repo, remove_content_units, base_version_pk, distr
102153
relationships = scoped_relationships
103154
remove_content_units.extend(str(pk) for pk in relationships.values_list("pk", flat=True))
104155

156+
if wildcard_removal:
157+
_prepare_release_removals(repository_version, remove_content_units, distribution, component)
158+
105159

106160
def _prepare_package_additions(add_content_units, distribution, component):
107161
"""Expand the addition list with the metadata needed to publish the packages in a component.

pulp_deb/tests/functional/api/test_repository_modify.py

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,74 @@ def test_remove_package_from_component(
163163
assert apt_package_release_components_api.list(**filters).count == expected_count
164164

165165

166+
def test_remove_package_from_all_distributions_and_components(
167+
apt_package_api,
168+
apt_package_release_components_api,
169+
apt_release_component_api,
170+
deb_get_repository_by_href,
171+
deb_modify_repository,
172+
deb_package_factory,
173+
deb_release_factory,
174+
deb_repository_factory,
175+
):
176+
repository = deb_repository_factory()
177+
removed_package = deb_package_factory(
178+
file=str(get_local_package_absolute_path(DEB_PACKAGE_RELPATH))
179+
)
180+
kept_package = deb_package_factory(
181+
file=str(
182+
get_local_package_absolute_path("odin_1.0_ppc64.deb", "data/debian/pool/asgard/o/odin/")
183+
)
184+
)
185+
distributions = [str(uuid4()), str(uuid4())]
186+
components = ["main", str(uuid4())]
187+
for distribution in distributions:
188+
deb_release_factory(
189+
codename=distribution,
190+
suite=distribution,
191+
distribution=distribution,
192+
repository=repository.pulp_href,
193+
)
194+
for distribution, component in (
195+
(distributions[0], components[0]),
196+
(distributions[0], components[1]),
197+
(distributions[1], components[0]),
198+
):
199+
_modify_with_package(
200+
repository,
201+
removed_package,
202+
deb_modify_repository,
203+
distribution=distribution,
204+
component=component,
205+
)
206+
_modify_with_package(
207+
repository,
208+
kept_package,
209+
deb_modify_repository,
210+
distribution=distributions[1],
211+
component=components[1],
212+
)
213+
214+
deb_modify_repository(
215+
repository,
216+
{
217+
"remove_content_units": [removed_package.pulp_href],
218+
"distribution": "*",
219+
"component": "*",
220+
},
221+
)
222+
repository = deb_get_repository_by_href(repository.pulp_href)
223+
224+
filters = {"repository_version": repository.latest_version_href}
225+
assert [item.pulp_href for item in apt_package_api.list(**filters).results] == [
226+
kept_package.pulp_href
227+
]
228+
assert [
229+
item.package for item in apt_package_release_components_api.list(**filters).results
230+
] == [kept_package.pulp_href]
231+
assert apt_release_component_api.list(**filters).count == 4
232+
233+
166234
def test_add_and_remove_packages_in_same_request(
167235
apt_package_api,
168236
apt_package_release_components_api,
@@ -210,6 +278,135 @@ def test_add_and_remove_packages_in_same_request(
210278
]
211279

212280

281+
def test_remove_all_packages_from_component(
282+
apt_package_api,
283+
apt_package_release_components_api,
284+
apt_release_api,
285+
apt_release_architecture_api,
286+
apt_release_component_api,
287+
deb_get_repository_by_href,
288+
deb_modify_repository,
289+
deb_package_factory,
290+
deb_release_factory,
291+
deb_repository_factory,
292+
):
293+
repository = deb_repository_factory()
294+
distribution = str(uuid4())
295+
components = ["main", str(uuid4())]
296+
packages = [
297+
deb_package_factory(file=str(get_local_package_absolute_path(DEB_PACKAGE_RELPATH))),
298+
deb_package_factory(
299+
file=str(
300+
get_local_package_absolute_path(
301+
"odin_1.0_ppc64.deb", "data/debian/pool/asgard/o/odin/"
302+
)
303+
)
304+
),
305+
]
306+
deb_release_factory(
307+
codename=distribution,
308+
suite=distribution,
309+
distribution=distribution,
310+
repository=repository.pulp_href,
311+
)
312+
for package, component in zip(packages, components):
313+
_modify_with_package(
314+
repository,
315+
package,
316+
deb_modify_repository,
317+
distribution=distribution,
318+
component=component,
319+
)
320+
321+
deb_modify_repository(
322+
repository,
323+
{
324+
"remove_content_units": ["*"],
325+
"distribution": distribution,
326+
"component": components[0],
327+
},
328+
)
329+
repository = deb_get_repository_by_href(repository.pulp_href)
330+
331+
filters = {"repository_version": repository.latest_version_href}
332+
assert [item.pulp_href for item in apt_package_api.list(**filters).results] == [
333+
packages[1].pulp_href
334+
]
335+
assert apt_package_release_components_api.list(**filters).count == 1
336+
# Only the emptied component goes away; the rest of the release is still in use.
337+
assert [item.component for item in apt_release_component_api.list(**filters).results] == [
338+
components[1]
339+
]
340+
assert apt_release_api.list(**filters).count == 1
341+
assert apt_release_architecture_api.list(**filters).count == 1
342+
343+
344+
def test_remove_all_packages_from_distribution(
345+
apt_package_api,
346+
apt_package_release_components_api,
347+
apt_release_api,
348+
apt_release_architecture_api,
349+
apt_release_component_api,
350+
deb_get_repository_by_href,
351+
deb_modify_repository,
352+
deb_package_factory,
353+
deb_release_factory,
354+
deb_repository_factory,
355+
):
356+
repository = deb_repository_factory()
357+
distributions = [str(uuid4()), str(uuid4())]
358+
packages = [
359+
deb_package_factory(file=str(get_local_package_absolute_path(DEB_PACKAGE_RELPATH))),
360+
deb_package_factory(
361+
file=str(
362+
get_local_package_absolute_path(
363+
"odin_1.0_ppc64.deb", "data/debian/pool/asgard/o/odin/"
364+
)
365+
)
366+
),
367+
]
368+
for package, distribution in zip(packages, distributions):
369+
deb_release_factory(
370+
codename=distribution,
371+
suite=distribution,
372+
distribution=distribution,
373+
repository=repository.pulp_href,
374+
)
375+
_modify_with_package(
376+
repository,
377+
package,
378+
deb_modify_repository,
379+
distribution=distribution,
380+
component="main",
381+
)
382+
383+
deb_modify_repository(
384+
repository,
385+
{
386+
"remove_content_units": ["*"],
387+
"distribution": distributions[0],
388+
"component": "*",
389+
},
390+
)
391+
repository = deb_get_repository_by_href(repository.pulp_href)
392+
393+
filters = {"repository_version": repository.latest_version_href}
394+
assert [item.pulp_href for item in apt_package_api.list(**filters).results] == [
395+
packages[1].pulp_href
396+
]
397+
assert apt_package_release_components_api.list(**filters).count == 1
398+
# Emptying a distribution takes its whole release structure with it.
399+
assert [item.distribution for item in apt_release_api.list(**filters).results] == [
400+
distributions[1]
401+
]
402+
assert [item.distribution for item in apt_release_component_api.list(**filters).results] == [
403+
distributions[1]
404+
]
405+
assert [item.distribution for item in apt_release_architecture_api.list(**filters).results] == [
406+
distributions[1]
407+
]
408+
409+
213410
def test_remove_all_content_units(
214411
apt_package_api,
215412
apt_package_release_components_api,
@@ -238,7 +435,14 @@ def test_remove_all_content_units(
238435
component=str(uuid4()),
239436
)
240437

241-
deb_modify_repository(repository, {"remove_content_units": ["*"]})
438+
deb_modify_repository(
439+
repository,
440+
{
441+
"remove_content_units": ["*"],
442+
"distribution": "*",
443+
"component": "*",
444+
},
445+
)
242446
repository = deb_get_repository_by_href(repository.pulp_href)
243447

244448
filters = {"repository_version": repository.latest_version_href}

0 commit comments

Comments
 (0)