Merge ~cjwatson/launchpad:built-using-guard-deletion into launchpad:master

Proposed by Colin Watson
Status: Needs review
Proposed branch: ~cjwatson/launchpad:built-using-guard-deletion
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:built-using-ui
Diff against target: 393 lines (+293/-1)
5 files modified
lib/lp/soyuz/interfaces/binarysourcereference.py (+15/-0)
lib/lp/soyuz/model/binarysourcereference.py (+29/-1)
lib/lp/soyuz/model/publishing.py (+54/-0)
lib/lp/soyuz/tests/test_binarysourcereference.py (+100/-0)
lib/lp/soyuz/tests/test_publishing.py (+95/-0)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+381239@code.launchpad.net

Commit message

Guard removal of sources referenced by Built-Using

Description of the change

Prevent SourcePackagePublishingHistory.requestDeletion from deleting source publications that have Built-Using references from active binary publications in the same archive and suite.

This isn't necessarily complete: in particular, it can miss references from other pockets, and in any case it might race with a build still in progress. The intent of this is not to ensure integrity, but to avoid some easily-detectable mistakes that could cause confusion.

To post a comment you must log in.
c0c4aa2... by Colin Watson

Expand deletion guard to other pockets

It now checks all pockets that could legitimately depend on the one from
which the publication is being deleted.

06ccc3e... by Colin Watson

Simplify tests using createFromSourcePackageReleases

Unmerged commits

06ccc3e... by Colin Watson

Simplify tests using createFromSourcePackageReleases

c0c4aa2... by Colin Watson

Expand deletion guard to other pockets

It now checks all pockets that could legitimately depend on the one from
which the publication is being deleted.

d7fbcfd... by Colin Watson

Guard removal of sources referenced by Built-Using

Prevent SourcePackagePublishingHistory.requestDeletion from deleting
source publications that have Built-Using references from active binary
publications in the same archive and suite.

This isn't necessarily complete: in particular, it can miss references
from other pockets, and in any case it might race with a build still in
progress. The intent of this is not to ensure integrity, but to avoid
some easily-detectable mistakes that could cause confusion.

LP: #1868558

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/soyuz/interfaces/binarysourcereference.py b/lib/lp/soyuz/interfaces/binarysourcereference.py
index dc21765..e625cf9 100644
--- a/lib/lp/soyuz/interfaces/binarysourcereference.py
+++ b/lib/lp/soyuz/interfaces/binarysourcereference.py
@@ -84,3 +84,18 @@ class IBinarySourceReferenceSet(Interface):
84 :param reference_type: A `BinarySourceReferenceType` to search for.84 :param reference_type: A `BinarySourceReferenceType` to search for.
85 :return: A `ResultSet` of matching `IBinarySourceReference`s.85 :return: A `ResultSet` of matching `IBinarySourceReference`s.
86 """86 """
87
88 def findPublished(archive, distroseries, pockets, reference_type,
89 source_package_releases=None):
90 """Find references corresponding to published binary publications.
91
92 :param archive: An `IArchive` to search for binary publications.
93 :param distroseries: An `IDistroSeries` to search for binary
94 publications.
95 :param pocket: A collection of `PackagePublishingPocket`s to search
96 for binary publications.
97 :param reference_type: A `BinarySourceReferenceType` to search for.
98 :param source_package_releases: If not None, only return references
99 pointing to any of this sequence of `ISourcePackageRelease`s.
100 :return: A `ResultSet` of matching `IBinarySourceReference`s.
101 """
diff --git a/lib/lp/soyuz/model/binarysourcereference.py b/lib/lp/soyuz/model/binarysourcereference.py
index adcfb6f..ca3563b 100644
--- a/lib/lp/soyuz/model/binarysourcereference.py
+++ b/lib/lp/soyuz/model/binarysourcereference.py
@@ -25,12 +25,17 @@ from lp.services.database.enumcol import DBEnum
25from lp.services.database.interfaces import IStore25from lp.services.database.interfaces import IStore
26from lp.services.database.stormbase import StormBase26from lp.services.database.stormbase import StormBase
27from lp.soyuz.adapters.archivedependencies import pocket_dependencies27from lp.soyuz.adapters.archivedependencies import pocket_dependencies
28from lp.soyuz.enums import BinarySourceReferenceType28from lp.soyuz.enums import (
29 BinarySourceReferenceType,
30 PackagePublishingStatus,
31 )
29from lp.soyuz.interfaces.binarysourcereference import (32from lp.soyuz.interfaces.binarysourcereference import (
30 IBinarySourceReference,33 IBinarySourceReference,
31 IBinarySourceReferenceSet,34 IBinarySourceReferenceSet,
32 UnparsableBuiltUsing,35 UnparsableBuiltUsing,
33 )36 )
37from lp.soyuz.model.distroarchseries import DistroArchSeries
38from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
3439
3540
36@implementer(IBinarySourceReference)41@implementer(IBinarySourceReference)
@@ -147,3 +152,26 @@ class BinarySourceReferenceSet:
147 BinarySourceReference,152 BinarySourceReference,
148 BinarySourceReference.binary_package_release == bpr,153 BinarySourceReference.binary_package_release == bpr,
149 BinarySourceReference.reference_type == reference_type)154 BinarySourceReference.reference_type == reference_type)
155
156 @classmethod
157 def findPublished(cls, archive, distroseries, pockets, reference_type,
158 source_package_releases=None):
159 """See `IBinarySourceReferenceSet`."""
160 clauses = [
161 BinarySourceReference.binary_package_release_id ==
162 BinaryPackagePublishingHistory.binarypackagereleaseID,
163 BinarySourceReference.reference_type == reference_type,
164 BinaryPackagePublishingHistory.status ==
165 PackagePublishingStatus.PUBLISHED,
166 BinaryPackagePublishingHistory.archive == archive,
167 BinaryPackagePublishingHistory.distroarchseries ==
168 DistroArchSeries.id,
169 BinaryPackagePublishingHistory.pocket.is_in(pockets),
170 DistroArchSeries.distroseries == distroseries,
171 ]
172 if source_package_releases is not None:
173 clauses.append(
174 BinarySourceReference.source_package_release_id.is_in(
175 spr.id for spr in source_package_releases))
176 return IStore(BinarySourceReference).find(
177 BinarySourceReference, *clauses).config(distinct=True)
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index fafd7f9..3c7ce7d 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -76,9 +76,11 @@ from lp.services.webapp.errorlog import (
76 ScriptRequest,76 ScriptRequest,
77 )77 )
78from lp.services.worlddata.model.country import Country78from lp.services.worlddata.model.country import Country
79from lp.soyuz.adapters.archivedependencies import pocket_dependencies
79from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias80from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
80from lp.soyuz.enums import (81from lp.soyuz.enums import (
81 BinaryPackageFormat,82 BinaryPackageFormat,
83 BinarySourceReferenceType,
82 PackagePublishingPriority,84 PackagePublishingPriority,
83 PackagePublishingStatus,85 PackagePublishingStatus,
84 PackageUploadStatus,86 PackageUploadStatus,
@@ -87,6 +89,9 @@ from lp.soyuz.interfaces.binarypackagebuild import (
87 BuildSetStatus,89 BuildSetStatus,
88 IBinaryPackageBuildSet,90 IBinaryPackageBuildSet,
89 )91 )
92from lp.soyuz.interfaces.binarysourcereference import (
93 IBinarySourceReferenceSet,
94 )
90from lp.soyuz.interfaces.component import IComponentSet95from lp.soyuz.interfaces.component import IComponentSet
91from lp.soyuz.interfaces.distributionjob import (96from lp.soyuz.interfaces.distributionjob import (
92 IDistroSeriesDifferenceJobSource,97 IDistroSeriesDifferenceJobSource,
@@ -603,6 +608,25 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
603 "Cannot delete publications from suite '%s'" %608 "Cannot delete publications from suite '%s'" %
604 self.distroseries.getSuite(self.pocket))609 self.distroseries.getSuite(self.pocket))
605610
611 # Check for active Built-Using references to this source package
612 # from any pockets in this archive and series that could
613 # legitimately refer to it. This isn't necessarily complete: it's
614 # there to avoid confusing consequences of mistakes, not to ensure
615 # integrity. (The dominator will reinstate a source publication if
616 # necessary.)
617 reverse_pockets = {
618 source_pocket
619 for source_pocket, expanded_pockets in pocket_dependencies.items()
620 if self.pocket in expanded_pockets}
621 bsrs = getUtility(IBinarySourceReferenceSet).findPublished(
622 self.archive, self.distroseries, reverse_pockets,
623 BinarySourceReferenceType.BUILT_USING,
624 source_package_releases=[self.sourcepackagerelease])
625 if not bsrs.is_empty():
626 raise DeletionError(
627 "Cannot delete source publication with a Built-Using "
628 "reference from an active binary publication.")
629
606 self.setDeleted(removed_by, removal_comment)630 self.setDeleted(removed_by, removal_comment)
607 if self.archive.is_main:631 if self.archive.is_main:
608 dsd_job_source = getUtility(IDistroSeriesDifferenceJobSource)632 dsd_job_source = getUtility(IDistroSeriesDifferenceJobSource)
@@ -1712,6 +1736,36 @@ class PublishingSet:
1712 "Cannot delete publications from suite '%s'" %1736 "Cannot delete publications from suite '%s'" %
1713 distroseries.getSuite(pocket))1737 distroseries.getSuite(pocket))
17141738
1739 # Check for active Built-Using references to these source packages
1740 # from any pockets in this archive and series that could
1741 # legitimately refer to them. This isn't necessarily complete: it's
1742 # there to avoid confusing consequences of mistakes, not to ensure
1743 # integrity. (The dominator will reinstate a source publication if
1744 # necessary.)
1745 if sources:
1746 # XXX cjwatson 2020-03-24: In theory we could consolidate this
1747 # into one query, but it's complex and probably not worth it.
1748 sprs_by_location = defaultdict(list)
1749 for spph in sources:
1750 sprs_by_location[
1751 (spph.archive, spph.distroseries, spph.pocket)].append(
1752 spph.sourcepackagerelease)
1753 for (archive, distroseries, pocket), sprs in (
1754 sprs_by_location.items()):
1755 reverse_pockets = {
1756 source_pocket
1757 for source_pocket, expanded_pockets
1758 in pocket_dependencies.items()
1759 if pocket in expanded_pockets}
1760 bsrs = getUtility(IBinarySourceReferenceSet).findPublished(
1761 archive, distroseries, reverse_pockets,
1762 BinarySourceReferenceType.BUILT_USING,
1763 source_package_releases=sprs)
1764 if not bsrs.is_empty():
1765 raise DeletionError(
1766 "Cannot delete source publication with a Built-Using "
1767 "reference from an active binary publication.")
1768
1715 spph_ids = [spph.id for spph in sources]1769 spph_ids = [spph.id for spph in sources]
1716 self.setMultipleDeleted(1770 self.setMultipleDeleted(
1717 SourcePackagePublishingHistory, spph_ids, removed_by,1771 SourcePackagePublishingHistory, spph_ids, removed_by,
diff --git a/lib/lp/soyuz/tests/test_binarysourcereference.py b/lib/lp/soyuz/tests/test_binarysourcereference.py
index f682de8..0c4db7f 100644
--- a/lib/lp/soyuz/tests/test_binarysourcereference.py
+++ b/lib/lp/soyuz/tests/test_binarysourcereference.py
@@ -20,6 +20,7 @@ from lp.registry.interfaces.pocket import PackagePublishingPocket
20from lp.soyuz.enums import (20from lp.soyuz.enums import (
21 ArchivePurpose,21 ArchivePurpose,
22 BinarySourceReferenceType,22 BinarySourceReferenceType,
23 PackagePublishingStatus,
23 )24 )
24from lp.soyuz.interfaces.binarysourcereference import (25from lp.soyuz.interfaces.binarysourcereference import (
25 IBinarySourceReferenceSet,26 IBinarySourceReferenceSet,
@@ -206,3 +207,102 @@ class TestBinarySourceReference(TestCaseWithFactory):
206 [],207 [],
207 self.reference_set.findByBinaryPackageRelease(208 self.reference_set.findByBinaryPackageRelease(
208 other_bpr, BinarySourceReferenceType.BUILT_USING))209 other_bpr, BinarySourceReferenceType.BUILT_USING))
210
211 def test_findPublished_empty(self):
212 archive = self.factory.makeArchive()
213 self.assertContentEqual(
214 [],
215 self.reference_set.findPublished(
216 archive, archive.distribution.currentseries,
217 {PackagePublishingPocket.RELEASE},
218 BinarySourceReferenceType.BUILT_USING))
219
220 def test_findPublished(self):
221 # findPublished finds references corresponding to published binary
222 # publications.
223 das = self.factory.makeDistroArchSeries()
224 archive = das.main_archive
225 bpphs = [
226 self.factory.makeBinaryPackagePublishingHistory(
227 archive=archive, distroarchseries=das, pocket=pocket,
228 status=PackagePublishingStatus.PUBLISHED)
229 for pocket in (
230 PackagePublishingPocket.RELEASE,
231 PackagePublishingPocket.PROPOSED)]
232 all_sprs = []
233 bsrs = []
234 for bpph in bpphs:
235 spphs = [
236 self.factory.makeSourcePackagePublishingHistory(
237 archive=bpph.archive, distroseries=bpph.distroseries,
238 pocket=bpph.pocket)
239 for _ in range(2)]
240 sprs = [spph.sourcepackagerelease for spph in spphs]
241 all_sprs.extend(sprs)
242 bsrs.extend(self.reference_set.createFromSourcePackageReleases(
243 bpph.binarypackagerelease, sprs,
244 BinarySourceReferenceType.BUILT_USING))
245
246 # Create a few more BPPHs with slight mismatches to ensure that
247 # findPublished matches correctly.
248 other_bpphs = []
249 other_bpphs.append(self.factory.makeBinaryPackagePublishingHistory(
250 archive=archive, pocket=PackagePublishingPocket.RELEASE,
251 status=PackagePublishingStatus.PUBLISHED))
252 other_bpphs.append(self.factory.makeBinaryPackagePublishingHistory(
253 archive=self.factory.makeArchive(
254 distribution=das.distroseries.distribution),
255 distroarchseries=das, pocket=PackagePublishingPocket.RELEASE,
256 status=PackagePublishingStatus.PUBLISHED))
257 other_bpphs.append(self.factory.makeBinaryPackagePublishingHistory(
258 archive=archive, distroarchseries=das,
259 pocket=PackagePublishingPocket.UPDATES,
260 status=PackagePublishingStatus.PUBLISHED))
261 other_bpphs.append(self.factory.makeBinaryPackagePublishingHistory(
262 archive=archive, distroarchseries=das,
263 pocket=PackagePublishingPocket.RELEASE,
264 status=PackagePublishingStatus.SUPERSEDED))
265 for bpph in other_bpphs:
266 spr = self.factory.makeSourcePackagePublishingHistory(
267 archive=bpph.archive, distroseries=bpph.distroseries,
268 pocket=bpph.pocket).sourcepackagerelease
269 self.reference_set.createFromSourcePackageReleases(
270 bpph.binarypackagerelease, [spr],
271 BinarySourceReferenceType.BUILT_USING)
272
273 self.assertThat(
274 self.reference_set.findPublished(
275 archive, das.distroseries, {PackagePublishingPocket.RELEASE},
276 BinarySourceReferenceType.BUILT_USING),
277 MatchesSetwise(*(
278 MatchesStructure.byEquality(
279 binary_package_release=bsr.binary_package_release,
280 source_package_release=bsr.source_package_release,
281 reference_type=BinarySourceReferenceType.BUILT_USING)
282 for bsr in bsrs[:2])))
283 self.assertThat(
284 self.reference_set.findPublished(
285 archive, das.distroseries,
286 {PackagePublishingPocket.RELEASE,
287 PackagePublishingPocket.PROPOSED},
288 BinarySourceReferenceType.BUILT_USING),
289 MatchesSetwise(*(
290 MatchesStructure.byEquality(
291 binary_package_release=bsr.binary_package_release,
292 source_package_release=bsr.source_package_release,
293 reference_type=BinarySourceReferenceType.BUILT_USING)
294 for bsr in bsrs)))
295 self.assertThat(
296 self.reference_set.findPublished(
297 archive, das.distroseries,
298 {PackagePublishingPocket.RELEASE,
299 PackagePublishingPocket.PROPOSED},
300 BinarySourceReferenceType.BUILT_USING,
301 source_package_releases=[
302 bsr.source_package_release for bsr in [bsrs[0], bsrs[2]]]),
303 MatchesSetwise(*(
304 MatchesStructure.byEquality(
305 binary_package_release=bsr.binary_package_release,
306 source_package_release=bsr.source_package_release,
307 reference_type=BinarySourceReferenceType.BUILT_USING)
308 for bsr in [bsrs[0], bsrs[2]])))
diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
index cd7d1ea..43d9b91 100644
--- a/lib/lp/soyuz/tests/test_publishing.py
+++ b/lib/lp/soyuz/tests/test_publishing.py
@@ -38,9 +38,13 @@ from lp.services.log.logger import DevNullLogger
38from lp.soyuz.enums import (38from lp.soyuz.enums import (
39 ArchivePurpose,39 ArchivePurpose,
40 BinaryPackageFormat,40 BinaryPackageFormat,
41 BinarySourceReferenceType,
41 PackageUploadStatus,42 PackageUploadStatus,
42 )43 )
43from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet44from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
45from lp.soyuz.interfaces.binarysourcereference import (
46 IBinarySourceReferenceSet,
47 )
44from lp.soyuz.interfaces.component import IComponentSet48from lp.soyuz.interfaces.component import IComponentSet
45from lp.soyuz.interfaces.publishing import (49from lp.soyuz.interfaces.publishing import (
46 active_publishing_status,50 active_publishing_status,
@@ -984,6 +988,97 @@ class TestPublishingSetLite(TestCaseWithFactory):
984 self.assertRaisesWithContent(988 self.assertRaisesWithContent(
985 DeletionError, message, pub.api_requestDeletion, self.person)989 DeletionError, message, pub.api_requestDeletion, self.person)
986990
991 def test_requestDeletion_singular_disallows_active_built_using_refs(self):
992 # The singular version of requestDeletion checks that there are no
993 # active Built-Using references to the source package being deleted.
994 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
995 distroseries = self.factory.makeDistroSeries(
996 distribution=archive.distribution)
997 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
998 bpphs = [
999 self.factory.makeBinaryPackagePublishingHistory(
1000 archive=archive, distroarchseries=das, pocket=pocket,
1001 status=PackagePublishingStatus.PUBLISHED)
1002 for pocket in (
1003 PackagePublishingPocket.RELEASE,
1004 PackagePublishingPocket.PROPOSED)]
1005 spph = self.factory.makeSourcePackagePublishingHistory(
1006 archive=archive, distroseries=distroseries, pocket=bpphs[0].pocket,
1007 status=PackagePublishingStatus.PUBLISHED)
1008 bsr_set = getUtility(IBinarySourceReferenceSet)
1009 for bpph in bpphs:
1010 spr = spph.sourcepackagerelease
1011 bsr_set.createFromSourcePackageReleases(
1012 bpph.binarypackagerelease, [spr],
1013 BinarySourceReferenceType.BUILT_USING)
1014
1015 # As long as the referring binary publications are active, deletion
1016 # is disallowed.
1017 message = (
1018 "Cannot delete source publication with a Built-Using reference "
1019 "from an active binary publication.")
1020 self.assertRaisesWithContent(
1021 DeletionError, message, spph.requestDeletion, self.person)
1022 self.assertRaisesWithContent(
1023 DeletionError, message, spph.api_requestDeletion, self.person)
1024 bpphs[0].requestDeletion(self.person)
1025 self.assertRaisesWithContent(
1026 DeletionError, message, spph.requestDeletion, self.person)
1027 self.assertRaisesWithContent(
1028 DeletionError, message, spph.api_requestDeletion, self.person)
1029 bpphs[1].requestDeletion(self.person)
1030
1031 # Now that all the referring binary publications are deleted, we
1032 # allow deleting the source publication.
1033 spph.requestDeletion(self.person)
1034
1035 def test_requestDeletion_bulk_disallows_active_built_using_refs(self):
1036 # The bulk version of requestDeletion checks that there are no
1037 # active Built-Using references to the source packages being
1038 # deleted.
1039 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
1040 distroseries = self.factory.makeDistroSeries(
1041 distribution=archive.distribution)
1042 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
1043 bpphs = [
1044 self.factory.makeBinaryPackagePublishingHistory(
1045 archive=archive, distroarchseries=das, pocket=pocket,
1046 status=PackagePublishingStatus.PUBLISHED)
1047 for pocket in (
1048 PackagePublishingPocket.RELEASE,
1049 PackagePublishingPocket.PROPOSED)]
1050 spphs = [
1051 self.factory.makeSourcePackagePublishingHistory(
1052 archive=archive, distroseries=distroseries,
1053 pocket=PackagePublishingPocket.RELEASE,
1054 status=PackagePublishingStatus.PUBLISHED)
1055 for bpph in bpphs]
1056 bsr_set = getUtility(IBinarySourceReferenceSet)
1057 for bpph in bpphs:
1058 for spph in spphs:
1059 spr = spph.sourcepackagerelease
1060 bsr_set.createFromSourcePackageReleases(
1061 bpph.binarypackagerelease, [spr],
1062 BinarySourceReferenceType.BUILT_USING)
1063
1064 # As long as the referring binary publications are active, deletion
1065 # is disallowed.
1066 message = (
1067 "Cannot delete source publication with a Built-Using reference "
1068 "from an active binary publication.")
1069 self.assertRaisesWithContent(
1070 DeletionError, message,
1071 getUtility(IPublishingSet).requestDeletion, spphs, self.person)
1072 bpphs[0].requestDeletion(self.person)
1073 self.assertRaisesWithContent(
1074 DeletionError, message,
1075 getUtility(IPublishingSet).requestDeletion, spphs, self.person)
1076 bpphs[1].requestDeletion(self.person)
1077
1078 # Now that all the referring binary publications are deleted, we
1079 # allow deleting the source publications.
1080 getUtility(IPublishingSet).requestDeletion(spphs, self.person)
1081
987 def test_requestDeletion_marks_debug_as_deleted(self):1082 def test_requestDeletion_marks_debug_as_deleted(self):
988 matching_bpph, debug_matching_bpph = (1083 matching_bpph, debug_matching_bpph = (
989 self.factory.makeBinaryPackagePublishingHistory(1084 self.factory.makeBinaryPackagePublishingHistory(

Subscribers

People subscribed via source and target branches

to status/vote changes: