← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-no-delete-with-pending-builds into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-no-delete-with-pending-builds into lp:launchpad.

Commit message:
Forbid deleting snap packages with pending builds.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-no-delete-with-pending-builds/+merge/294712

Forbid deleting snap packages with pending builds.

This is a compromise between the original rather over-the-top restriction (no deletion of packages that have ever had any builds) and the current state, which results in build farm chaos if somebody happens to delete a snap that has pending builds because now we end up with BuildQueue and BuildFarmJob rows that have no specific build.  I considered dealing with the pending-build case instead, but it's potentially rather slow, especially if builds are actually running in which case we'd have to arrange to finish the deletion after the builders finish cancelling.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-no-delete-with-pending-builds into lp:launchpad.
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml	2016-05-06 11:54:18 +0000
+++ lib/lp/snappy/browser/configure.zcml	2016-05-14 15:34:09 +0000
@@ -71,7 +71,7 @@
             class="lp.snappy.browser.snap.SnapDeleteView"
             permission="launchpad.Edit"
             name="+delete"
-            template="../../app/templates/generic-edit.pt" />
+            template="../templates/snap-delete.pt" />
         <adapter
             provides="lp.services.webapp.interfaces.IBreadcrumb"
             for="lp.snappy.interfaces.snap.ISnap"

=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py	2016-05-06 09:45:45 +0000
+++ lib/lp/snappy/browser/snap.py	2016-05-14 15:34:09 +0000
@@ -514,6 +514,10 @@
 
     field_names = []
 
+    @property
+    def has_pending_builds(self):
+        return not self.context.pending_builds.is_empty()
+
     @action('Delete snap package', name='delete')
     def delete_action(self, action, data):
         owner = self.context.owner

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2016-05-06 09:45:45 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2016-05-14 15:34:09 +0000
@@ -594,12 +594,24 @@
         self.assertEqual(owner_url + "/+snaps", browser.url)
         self.assertRaises(NotFound, browser.open, snap_url)
 
+    def test_delete_snap_with_pending_builds(self):
+        # A snap package with pending builds cannot be deleted.
+        self.useFixture(FakeLogger())
+        snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
+        self.factory.makeSnapBuild(snap=snap, status=BuildStatus.NEEDSBUILD)
+        browser = self.getViewBrowser(snap, user=self.person)
+        browser.getLink("Delete snap package").click()
+        self.assertIn("This snap package cannot be deleted", browser.contents)
+        self.assertRaises(
+            LookupError, browser.getControl, "Delete snap package")
+
     def test_delete_snap_with_builds(self):
-        # A snap package with builds can be deleted.
+        # A snap package with completed builds can be deleted.
         self.useFixture(FakeLogger())
         snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
-        build = self.factory.makeSnapBuild(snap=snap)
-        self.factory.makeSnapFile(snapbuild=build)
+        for status in (BuildStatus.FULLYBUILT, BuildStatus.CANCELLED):
+            build = self.factory.makeSnapBuild(snap=snap, status=status)
+            self.factory.makeSnapFile(snapbuild=build)
         snap_url = canonical_url(snap)
         owner_url = canonical_url(self.person)
         browser = self.getViewBrowser(snap, user=self.person)

=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/interfaces/snap.py	2016-05-14 15:34:09 +0000
@@ -7,6 +7,7 @@
 
 __all__ = [
     'BadSnapSearchContext',
+    'CannotDeleteSnap',
     'CannotModifySnapProcessor',
     'DuplicateSnapName',
     'ISnap',
@@ -197,6 +198,11 @@
             "Snap contains private information and cannot be public.")
 
 
+@error_status(httplib.BAD_REQUEST)
+class CannotDeleteSnap(Exception):
+    """This snap package cannot be deleted."""
+
+
 class BadSnapSearchContext(Exception):
     """The context is not valid for a snap package search."""
 
@@ -302,7 +308,7 @@
     @export_destructor_operation()
     @operation_for_version("devel")
     def destroySelf():
-        """Delete this snap package, provided that it has no builds."""
+        """Delete this snap package, provided that it has no pending builds."""
 
 
 class ISnapEditableAttributes(IHasOwner):

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/model/snap.py	2016-05-14 15:34:09 +0000
@@ -77,6 +77,7 @@
 from lp.services.webhooks.model import WebhookTargetMixin
 from lp.snappy.interfaces.snap import (
     BadSnapSearchContext,
+    CannotDeleteSnap,
     CannotModifySnapProcessor,
     DuplicateSnapName,
     ISnap,
@@ -381,6 +382,9 @@
 
     def destroySelf(self):
         """See `ISnap`."""
+        if not self.pending_builds.is_empty():
+            raise CannotDeleteSnap(
+                "Cannot delete a snap package with pending builds.")
         store = IStore(Snap)
         store.find(SnapArch, SnapArch.snap == self).remove()
         # XXX cjwatson 2016-02-27 bug=322972: Requires manual SQL due to

=== added file 'lib/lp/snappy/templates/snap-delete.pt'
--- lib/lp/snappy/templates/snap-delete.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-delete.pt	2016-05-14 15:34:09 +0000
@@ -0,0 +1,21 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad">
+<body>
+
+  <div metal:fill-slot="main">
+    <tal:has-builds condition="view/has_pending_builds">
+      This snap package cannot be deleted as it has pending builds.
+    </tal:has-builds>
+
+    <tal:has-no-builds condition="not: view/has_pending_builds">
+      <div metal:use-macro="context/@@launchpad_form/form"/>
+    </tal:has-no-builds>
+  </div>
+
+</body>
+</html>

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2016-05-06 09:45:45 +0000
+++ lib/lp/snappy/tests/test_snap.py	2016-05-14 15:34:09 +0000
@@ -38,6 +38,7 @@
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.snappy.interfaces.snap import (
     BadSnapSearchContext,
+    CannotDeleteSnap,
     CannotModifySnapProcessor,
     ISnap,
     ISnapSet,
@@ -373,25 +374,45 @@
         super(TestSnapDeleteWithBuilds, self).setUp()
         self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
 
+    def test_delete_with_pending_builds(self):
+        # A snap package with pending builds cannot be deleted.
+        owner = self.factory.makePerson()
+        distroseries = self.factory.makeDistroSeries()
+        snap = self.factory.makeSnap(
+            registrant=owner, owner=owner, distroseries=distroseries,
+            name=u"condemned")
+        self.factory.makeSnapBuild(snap=snap, status=BuildStatus.NEEDSBUILD)
+        self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+        with person_logged_in(snap.owner):
+            self.assertRaises(CannotDeleteSnap, snap.destroySelf)
+        self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+
     def test_delete_with_builds(self):
-        # A snap package with builds can be deleted.  Doing so deletes all
-        # its builds and their files too.
+        # A snap package with completed builds can be deleted.  Doing so
+        # deletes all its builds and their files too.
         owner = self.factory.makePerson()
         distroseries = self.factory.makeDistroSeries()
         snap = self.factory.makeSnap(
             registrant=owner, owner=owner, distroseries=distroseries,
             name=u"condemned")
-        build = self.factory.makeSnapBuild(snap=snap)
-        snapfile = self.factory.makeSnapFile(snapbuild=build)
+        fullybuilt_build = self.factory.makeSnapBuild(
+            snap=snap, status=BuildStatus.FULLYBUILT)
+        snapfile = self.factory.makeSnapFile(snapbuild=fullybuilt_build)
         self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
-        build_id = build.id
+        fullybuilt_build_id = fullybuilt_build.id
         snapfile_id = removeSecurityProxy(snapfile).id
+        cancelled_build = self.factory.makeSnapBuild(
+            snap=snap, status=BuildStatus.CANCELLED)
+        cancelled_build_id = cancelled_build.id
         with person_logged_in(snap.owner):
             snap.destroySelf()
         flush_database_caches()
         self.assertFalse(getUtility(ISnapSet).exists(owner, u"condemned"))
-        self.assertIsNone(getUtility(ISnapBuildSet).getByID(build_id))
+        self.assertIsNone(
+            getUtility(ISnapBuildSet).getByID(fullybuilt_build_id))
         self.assertIsNone(IMasterStore(SnapFile).get(SnapFile, snapfile_id))
+        self.assertIsNone(
+            getUtility(ISnapBuildSet).getByID(cancelled_build_id))
 
     def test_related_webhooks_deleted(self):
         owner = self.factory.makePerson()


Follow ups