← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-build-channels into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-build-channels into lp:launchpad.

Commit message:
Add the ability to select source channels when building snaps.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1737994 in Launchpad itself: "build.snapcraft.io uses snapcraft from the deb archive"
  https://bugs.launchpad.net/launchpad/+bug/1737994

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-build-channels/+merge/337360

This will let us run snap builds where snapcraft is installed as a snap.  It depends on https://code.launchpad.net/~cjwatson/launchpad-buildd/snapception/+merge/337126 and https://code.launchpad.net/~cjwatson/launchpad/db-snap-build-channels/+merge/337359.

We'll probably also want a feature flag to change the default, but that can be added a little later, once the basic mechanism works.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-build-channels into lp:launchpad.
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2017-08-22 11:36:30 +0000
+++ lib/lp/snappy/interfaces/snap.py	2018-02-08 13:37:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Snap package interfaces."""
@@ -63,6 +63,7 @@
     Bool,
     Choice,
     Datetime,
+    Dict,
     Int,
     List,
     Text,
@@ -281,17 +282,23 @@
     @operation_parameters(
         archive=Reference(schema=IArchive),
         distro_arch_series=Reference(schema=IDistroArchSeries),
-        pocket=Choice(vocabulary=PackagePublishingPocket))
+        pocket=Choice(vocabulary=PackagePublishingPocket),
+        channels=Dict(
+            title=_("Source channels to use for this build."),
+            key_type=TextLine(), required=False))
     # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
     @export_factory_operation(Interface, [])
     @operation_for_version("devel")
-    def requestBuild(requester, archive, distro_arch_series, pocket):
+    def requestBuild(requester, archive, distro_arch_series, pocket,
+                     channels=None):
         """Request that the snap package be built.
 
         :param requester: The person requesting the build.
         :param archive: The IArchive to associate the build with.
         :param distro_arch_series: The architecture to build for.
         :param pocket: The pocket that should be targeted.
+        :param channels: A dictionary mapping snap names to channels to use
+            for this build.
         :return: `ISnapBuild`.
         """
 
@@ -509,6 +516,14 @@
             "The package stream within the source distribution series to use "
             "when building the snap package.")))
 
+    auto_build_channels = exported(Dict(
+        title=_("Source channels for automatic builds"),
+        key_type=TextLine(), required=False, readonly=False,
+        description=_(
+            "A dictionary mapping snap names to channels to use when building "
+            "this snap package.  Currently only 'core' and 'snapcraft' keys "
+            "are supported.")))
+
     is_stale = Bool(
         title=_("Snap package is stale and is due to be rebuilt."),
         required=True, readonly=False)

=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py	2017-04-27 16:22:37 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py	2018-02-08 13:37:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Snap package build interfaces."""
@@ -39,6 +39,7 @@
     Bool,
     Choice,
     Datetime,
+    Dict,
     Int,
     TextLine,
     )
@@ -147,6 +148,13 @@
         title=_("The pocket for which to build."),
         vocabulary=PackagePublishingPocket, required=True, readonly=True))
 
+    channels = exported(Dict(
+        title=_("Source channels to use for this build."),
+        description=_(
+            "A dictionary mapping snap names to channels to use for this "
+            "build.  Currently only 'core' and 'snapcraft' keys are "
+            "supported.")))
+
     virtualized = Bool(
         title=_("If True, this build is virtualized."), readonly=True)
 

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2017-11-10 11:23:27 +0000
+++ lib/lp/snappy/model/snap.py	2018-02-08 13:37:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -189,6 +189,8 @@
 
     auto_build_pocket = DBEnum(enum=PackagePublishingPocket, allow_none=True)
 
+    auto_build_channels = JSON('auto_build_channels', allow_none=True)
+
     is_stale = Bool(name='is_stale', allow_none=False)
 
     require_virtualized = Bool(name='require_virtualized')
@@ -209,9 +211,10 @@
     def __init__(self, registrant, owner, distro_series, name,
                  description=None, branch=None, git_ref=None, auto_build=False,
                  auto_build_archive=None, auto_build_pocket=None,
-                 require_virtualized=True, date_created=DEFAULT,
-                 private=False, store_upload=False, store_series=None,
-                 store_name=None, store_secrets=None, store_channels=None):
+                 auto_build_channels=None, require_virtualized=True,
+                 date_created=DEFAULT, private=False, store_upload=False,
+                 store_series=None, store_name=None, store_secrets=None,
+                 store_channels=None):
         """Construct a `Snap`."""
         super(Snap, self).__init__()
         self.registrant = registrant
@@ -224,6 +227,7 @@
         self.auto_build = auto_build
         self.auto_build_archive = auto_build_archive
         self.auto_build_pocket = auto_build_pocket
+        self.auto_build_channels = auto_build_channels
         self.require_virtualized = require_virtualized
         self.date_created = date_created
         self.date_last_modified = date_created
@@ -439,7 +443,8 @@
             return False
         return True
 
-    def requestBuild(self, requester, archive, distro_arch_series, pocket):
+    def requestBuild(self, requester, archive, distro_arch_series, pocket,
+                     channels=None):
         """See `ISnap`."""
         if not requester.inTeam(self.owner):
             raise SnapNotOwner(
@@ -459,12 +464,14 @@
             SnapBuild.archive_id == archive.id,
             SnapBuild.distro_arch_series_id == distro_arch_series.id,
             SnapBuild.pocket == pocket,
+            SnapBuild.channels == channels,
             SnapBuild.status == BuildStatus.NEEDSBUILD)
         if pending.any() is not None:
             raise SnapBuildAlreadyPending
 
         build = getUtility(ISnapBuildSet).new(
-            requester, self, archive, distro_arch_series, pocket)
+            requester, self, archive, distro_arch_series, pocket,
+            channels=channels)
         build.queueBuild()
         return build
 
@@ -484,7 +491,7 @@
             try:
                 build = self.requestBuild(
                     self.owner, self.auto_build_archive, arch,
-                    self.auto_build_pocket)
+                    self.auto_build_pocket, self.auto_build_channels)
                 if logger is not None:
                     logger.debug(
                         " - %s/%s/%s: Build requested.",
@@ -659,9 +666,10 @@
             branch=None, git_repository=None, git_repository_url=None,
             git_path=None, git_ref=None, auto_build=False,
             auto_build_archive=None, auto_build_pocket=None,
-            require_virtualized=True, processors=None, date_created=DEFAULT,
-            private=False, store_upload=False, store_series=None,
-            store_name=None, store_secrets=None, store_channels=None):
+            auto_build_channels=None, require_virtualized=True,
+            processors=None, date_created=DEFAULT, private=False,
+            store_upload=False, store_series=None, store_name=None,
+            store_secrets=None, store_channels=None):
         """See `ISnapSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
@@ -702,6 +710,7 @@
             branch=branch, git_ref=git_ref, auto_build=auto_build,
             auto_build_archive=auto_build_archive,
             auto_build_pocket=auto_build_pocket,
+            auto_build_channels=auto_build_channels,
             require_virtualized=require_virtualized, date_created=date_created,
             private=private, store_upload=store_upload,
             store_series=store_series, store_name=store_name,
@@ -917,6 +926,15 @@
                     SnapBuild.snap_id == Snap.id,
                     SnapBuild.archive_id == Snap.auto_build_archive_id,
                     SnapBuild.pocket == Snap.auto_build_pocket,
+                    # These columns are nullable so require some care, since
+                    # a straightforward equality check will compile to
+                    # "SnapBuild.channels = Snap.auto_build_channels" which
+                    # is false if both are NULL.
+                    Or(
+                        And(
+                            SnapBuild.channels == None,
+                            Snap.auto_build_channels == None),
+                        SnapBuild.channels == Snap.auto_build_channels),
                     # We only want Snaps that haven't had an automatic
                     # SnapBuild dispatched for them recently.
                     SnapBuild.date_created >= threshold_date)),

=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py	2018-01-23 10:59:44 +0000
+++ lib/lp/snappy/model/snapbuild.py	2018-02-08 13:37:58 +0000
@@ -22,6 +22,7 @@
     DateTime,
     Desc,
     Int,
+    JSON,
     Reference,
     Select,
     SQL,
@@ -142,6 +143,8 @@
 
     pocket = DBEnum(enum=PackagePublishingPocket, allow_none=False)
 
+    channels = JSON('channels', allow_none=True)
+
     processor_id = Int(name='processor', allow_none=False)
     processor = Reference(processor_id, 'Processor.id')
     virtualized = Bool(name='virtualized')
@@ -171,7 +174,7 @@
     failure_count = Int(name='failure_count', allow_none=False)
 
     def __init__(self, build_farm_job, requester, snap, archive,
-                 distro_arch_series, pocket, processor, virtualized,
+                 distro_arch_series, pocket, channels, processor, virtualized,
                  date_created):
         """Construct a `SnapBuild`."""
         super(SnapBuild, self).__init__()
@@ -181,6 +184,7 @@
         self.archive = archive
         self.distro_arch_series = distro_arch_series
         self.pocket = pocket
+        self.channels = channels
         self.processor = processor
         self.virtualized = virtualized
         self.date_created = date_created
@@ -479,7 +483,7 @@
 class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
 
     def new(self, requester, snap, archive, distro_arch_series, pocket,
-            date_created=DEFAULT):
+            channels=None, date_created=DEFAULT):
         """See `ISnapBuildSet`."""
         store = IMasterStore(SnapBuild)
         build_farm_job = getUtility(IBuildFarmJobSource).new(
@@ -487,7 +491,7 @@
             archive)
         snapbuild = SnapBuild(
             build_farm_job, requester, snap, archive, distro_arch_series,
-            pocket, distro_arch_series.processor,
+            pocket, channels, distro_arch_series.processor,
             not distro_arch_series.processor.supports_nonvirtualized
             or snap.require_virtualized or archive.require_virtualized,
             date_created)

=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py	2017-07-25 18:01:04 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py	2018-02-08 13:37:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """An `IBuildFarmJobBehaviour` for `SnapBuild`.
@@ -107,6 +107,8 @@
                 tools_fingerprint=config.snappy.tools_fingerprint,
                 logger=logger))
         args["archive_private"] = build.archive.private
+        if build.channels is not None:
+            args["channels"] = build.channels
         if build.snap.branch is not None:
             args["branch"] = build.snap.branch.bzr_identity
         elif build.snap.git_ref is not None:

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2018-01-23 18:54:30 +0000
+++ lib/lp/snappy/tests/test_snap.py	2018-02-08 13:37:58 +0000
@@ -54,6 +54,7 @@
     ONE_DAY_AGO,
     UTC_NOW,
     )
+from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import flush_database_caches
 from lp.services.features.testing import (
     FeatureFixture,
@@ -188,6 +189,7 @@
         self.assertEqual(snap.distro_series.main_archive, build.archive)
         self.assertEqual(distroarchseries, build.distro_arch_series)
         self.assertEqual(PackagePublishingPocket.UPDATES, build.pocket)
+        self.assertIsNone(build.channels)
         self.assertEqual(BuildStatus.NEEDSBUILD, build.status)
         store = Store.of(build)
         store.flush()
@@ -232,6 +234,18 @@
         queue_record.score()
         self.assertEqual(2610, queue_record.lastscore)
 
+    def test_requestBuild_channels(self):
+        # requestBuild can select non-default channels.
+        processor = self.factory.makeProcessor(supports_virtualized=True)
+        distroarchseries = self.makeBuildableDistroArchSeries(
+            processor=processor)
+        snap = self.factory.makeSnap(
+            distroseries=distroarchseries.distroseries, processors=[processor])
+        build = snap.requestBuild(
+            snap.owner, snap.distro_series.main_archive, distroarchseries,
+            PackagePublishingPocket.UPDATES, channels={"snapcraft": "edge"})
+        self.assertEqual({"snapcraft": "edge"}, build.channels)
+
     def test_requestBuild_rejects_repeats(self):
         # requestBuild refuses if there is already a pending build.
         distroseries = self.factory.makeDistroSeries()
@@ -355,10 +369,36 @@
         with person_logged_in(snap.owner):
             builds = snap.requestAutoBuilds()
         self.assertThat(builds, MatchesSetwise(
+            *(MatchesStructure(
+                requester=Equals(snap.owner), snap=Equals(snap),
+                archive=Equals(archive), distro_arch_series=Equals(das),
+                pocket=Equals(PackagePublishingPocket.PROPOSED),
+                channels=Is(None))
+              for das in dases[:2])))
+
+    def test_requestAutoBuilds_channels(self):
+        # requestAutoBuilds honours Snap.auto_build_channels.
+        distroseries = self.factory.makeDistroSeries()
+        dases = []
+        for _ in range(3):
+            processor = self.factory.makeProcessor(supports_virtualized=True)
+            dases.append(self.makeBuildableDistroArchSeries(
+                distroseries=distroseries, processor=processor))
+        archive = self.factory.makeArchive()
+        snap = self.factory.makeSnap(
+            distroseries=distroseries,
+            processors=[das.processor for das in dases[:2]],
+            auto_build_archive=archive,
+            auto_build_pocket=PackagePublishingPocket.PROPOSED,
+            auto_build_channels={"snapcraft": "edge"})
+        with person_logged_in(snap.owner):
+            builds = snap.requestAutoBuilds()
+        self.assertThat(builds, MatchesSetwise(
             *(MatchesStructure.byEquality(
                 requester=snap.owner, snap=snap, archive=archive,
                 distro_arch_series=das,
-                pocket=PackagePublishingPocket.PROPOSED)
+                pocket=PackagePublishingPocket.PROPOSED,
+                channels={"snapcraft": "edge"})
               for das in dases[:2])))
 
     def test_getBuilds(self):
@@ -610,6 +650,7 @@
         self.assertFalse(snap.auto_build)
         self.assertIsNone(snap.auto_build_archive)
         self.assertIsNone(snap.auto_build_pocket)
+        self.assertIsNone(snap.auto_build_channels)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
 
@@ -630,6 +671,7 @@
         self.assertFalse(snap.auto_build)
         self.assertIsNone(snap.auto_build_archive)
         self.assertIsNone(snap.auto_build_pocket)
+        self.assertIsNone(snap.auto_build_channels)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
 
@@ -1008,6 +1050,56 @@
         self.assertEqual([], builds)
         self.assertEqual([], logger.getLogBuffer().splitlines())
 
+    def test_makeAutoBuilds_skips_if_built_recently_matching_channels(self):
+        # ISnapSet.makeAutoBuilds only considers recently-requested builds
+        # to match a snap if they match its auto_build_channels.
+        das1, snap1 = self.makeAutoBuildableSnap(is_stale=True)
+        das2, snap2 = self.makeAutoBuildableSnap(
+            is_stale=True, auto_build_channels={"snapcraft": "edge"})
+        # Create some builds with mismatched channels.
+        self.factory.makeSnapBuild(
+            requester=snap1.owner, snap=snap1,
+            archive=snap1.auto_build_archive, distroarchseries=das1,
+            channels={"snapcraft": "edge"})
+        self.factory.makeSnapBuild(
+            requester=snap2.owner, snap=snap2,
+            archive=snap2.auto_build_archive, distroarchseries=das2,
+            channels={"snapcraft": "stable"})
+
+        logger = BufferLogger()
+        builds = getUtility(ISnapSet).makeAutoBuilds(logger=logger)
+        self.assertThat(builds, MatchesSetwise(
+            MatchesStructure(
+                requester=Equals(snap1.owner), snap=Equals(snap1),
+                distro_arch_series=Equals(das1), channels=Is(None),
+                status=Equals(BuildStatus.NEEDSBUILD)),
+            MatchesStructure.byEquality(
+                requester=snap2.owner, snap=snap2, distro_arch_series=das2,
+                channels={"snapcraft": "edge"}, status=BuildStatus.NEEDSBUILD),
+            ))
+        log_entries = logger.getLogBuffer().splitlines()
+        self.assertEqual(4, len(log_entries))
+        for das, snap in (das1, snap1), (das2, snap2):
+            self.assertIn(
+                "DEBUG Scheduling builds of snap package %s/%s" % (
+                    snap.owner.name, snap.name),
+                log_entries)
+            self.assertIn(
+                "DEBUG  - %s/%s/%s: Build requested." % (
+                    snap.owner.name, snap.name, das.architecturetag),
+                log_entries)
+            self.assertFalse(snap.is_stale)
+
+        # Mark the two snaps stale and try again.  There are now matching
+        # builds so we don't try to request more.
+        for snap in snap1, snap2:
+            removeSecurityProxy(snap).is_stale = True
+            IStore(snap).flush()
+        logger = BufferLogger()
+        builds = getUtility(ISnapSet).makeAutoBuilds(logger=logger)
+        self.assertEqual([], builds)
+        self.assertEqual([], logger.getLogBuffer().splitlines())
+
     def test_makeAutoBuilds_skips_non_stale_snaps(self):
         # ISnapSet.makeAutoBuilds skips snap packages that are not stale.
         das, snap = self.makeAutoBuildableSnap(is_stale=False)

=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py	2018-01-23 10:59:44 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py	2018-02-08 13:37:58 +0000
@@ -597,6 +597,7 @@
             self.assertEqual(
                 db_build.distro_arch_series.architecturetag, build["arch_tag"])
             self.assertEqual("Updates", build["pocket"])
+            self.assertIsNone(build["channels"])
             self.assertIsNone(build["score"])
             self.assertFalse(build["can_be_rescored"])
             self.assertFalse(build["can_be_cancelled"])

=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py	2017-10-20 13:35:42 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py	2018-02-08 13:37:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test snap package build behaviour."""
@@ -396,6 +396,17 @@
             ]))
 
     @defer.inlineCallbacks
+    def test_extraBuildArgs_channels(self):
+        # If the build needs particular channels, _extraBuildArgs sends
+        # them.
+        job = self.makeJob(channels={"snapcraft": "edge"})
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, job.build.distro_arch_series, None))
+        args = yield job._extraBuildArgs()
+        self.assertEqual({"snapcraft": "edge"}, args["channels"])
+
+    @defer.inlineCallbacks
     def test_composeBuildRequest_proxy_url_set(self):
         job = self.makeJob()
         build_request = yield job.composeBuildRequest(None)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2017-04-25 11:36:10 +0000
+++ lib/lp/testing/factory.py	2018-02-08 13:37:58 +0000
@@ -2,7 +2,7 @@
 # NOTE: The first line above must stay first; do not move the copyright
 # notice to the top.  See http://www.python.org/dev/peps/pep-0263/.
 #
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Testing infrastructure for the Launchpad application.
@@ -4649,7 +4649,8 @@
     def makeSnap(self, registrant=None, owner=None, distroseries=None,
                  name=None, branch=None, git_ref=None, auto_build=False,
                  auto_build_archive=None, auto_build_pocket=None,
-                 is_stale=None, require_virtualized=True, processors=None,
+                 auto_build_channels=None, is_stale=None,
+                 require_virtualized=True, processors=None,
                  date_created=DEFAULT, private=False, store_upload=False,
                  store_series=None, store_name=None, store_secrets=None,
                  store_channels=None):
@@ -4675,7 +4676,8 @@
             require_virtualized=require_virtualized, processors=processors,
             date_created=date_created, branch=branch, git_ref=git_ref,
             auto_build=auto_build, auto_build_archive=auto_build_archive,
-            auto_build_pocket=auto_build_pocket, private=private,
+            auto_build_pocket=auto_build_pocket,
+            auto_build_channels=auto_build_channels, private=private,
             store_upload=store_upload, store_series=store_series,
             store_name=store_name, store_secrets=store_secrets,
             store_channels=store_channels)
@@ -4686,8 +4688,9 @@
 
     def makeSnapBuild(self, requester=None, registrant=None, snap=None,
                       archive=None, distroarchseries=None, pocket=None,
-                      date_created=DEFAULT, status=BuildStatus.NEEDSBUILD,
-                      builder=None, duration=None, **kwargs):
+                      channels=None, date_created=DEFAULT,
+                      status=BuildStatus.NEEDSBUILD, builder=None,
+                      duration=None, **kwargs):
         """Make a new SnapBuild."""
         if requester is None:
             requester = self.makePerson()
@@ -4715,7 +4718,7 @@
             pocket = PackagePublishingPocket.UPDATES
         snapbuild = getUtility(ISnapBuildSet).new(
             requester, snap, archive, distroarchseries, pocket,
-            date_created=date_created)
+            channels=channels, date_created=date_created)
         if duration is not None:
             removeSecurityProxy(snapbuild).updateStatus(
                 BuildStatus.BUILDING, builder=builder,


Follow ups