← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-allow-network into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-allow-network into lp:launchpad.

Commit message:
Add Snap.allow_network: if false, do not dispatch a proxy token to builds of that snap.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-allow-network/+merge/336924

This will allow snaps that are intended to be delivered with Ubuntu images to be restricted to build from only resources on Launchpad, and thus be reproducible, supportable, etc.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-allow-network into lp:launchpad.
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py	2016-11-12 21:02:10 +0000
+++ lib/lp/scripts/garbo.py	2018-01-31 14:51:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 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).
 
 """Database garbage collection."""
@@ -41,6 +41,7 @@
     Or,
     Row,
     SQL,
+    Update,
     )
 from storm.info import ClassAlias
 from storm.store import EmptyResultSet
@@ -1603,6 +1604,32 @@
         transaction.commit()
 
 
+class SnapAllowNetworkPopulator(TunableLoop):
+    """Populates Snap.allow_network with True."""
+
+    maximum_chunk_size = 5000
+
+    def __init__(self, log, abort_time=None):
+        super(SnapAllowNetworkPopulator, self).__init__(log, abort_time)
+        self.start_at = 1
+        self.store = IMasterStore(Snap)
+
+    def findSnaps(self):
+        return self.store.find(
+            Snap,
+            Snap.id >= self.start_at,
+            Snap._allow_network == None).order_by(Snap.id)
+
+    def isDone(self):
+        return self.findSnaps().is_empty()
+
+    def __call__(self, chunk_size):
+        ids = [snap.id for snap in self.findSnaps()]
+        self.store.execute(Update(
+            {Snap._allow_network: True}, where=Snap.id.is_in(ids), table=Snap))
+        transaction.commit()
+
+
 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
     """Abstract base class to run a collection of TunableLoops."""
     script_name = None  # Script name for locking and database user. Override.
@@ -1893,6 +1920,7 @@
         ProductVCSPopulator,
         RevisionAuthorEmailLinker,
         ScrubPOFileTranslator,
+        SnapAllowNetworkPopulator,
         SnapBuildJobPruner,
         SnapStoreSeriesPopulator,
         SuggestiveTemplatesCacheUpdater,

=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py	2018-01-02 10:54:31 +0000
+++ lib/lp/scripts/tests/test_garbo.py	2018-01-31 14:51:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 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).
 
 """Test the database garbage collector."""
@@ -15,6 +15,7 @@
 from StringIO import StringIO
 import time
 
+from psycopg2 import IntegrityError
 from pytz import UTC
 from storm.exceptions import LostObjectError
 from storm.expr import (
@@ -1553,6 +1554,34 @@
         # Snaps with more than one possible store series are untouched.
         self.assertIsNone(snaps[5].store_series)
 
+    def test_SnapAllowNetworkPopulator(self):
+        switch_dbuser('testadmin')
+        old_snaps = [self.factory.makeSnap() for _ in range(2)]
+        for snap in old_snaps:
+            removeSecurityProxy(snap)._allow_network = None
+        try:
+            Store.of(old_snaps[0]).flush()
+        except IntegrityError:
+            # Now enforced by DB NOT NULL constraint; backfilling is no
+            # longer necessary.
+            return
+        allow_network_snaps = [
+            self.factory.makeSnap(allow_network=True) for _ in range(2)]
+        disallow_network_snaps = [
+            self.factory.makeSnap(allow_network=False) for _ in range(2)]
+        transaction.commit()
+
+        self.runDaily()
+
+        # Old snaps are backfilled.
+        for snap in old_snaps:
+            self.assertIs(True, removeSecurityProxy(snap)._allow_network)
+        # Other snaps are left alone.
+        for snap in allow_network_snaps:
+            self.assertIs(True, removeSecurityProxy(snap)._allow_network)
+        for snap in disallow_network_snaps:
+            self.assertIs(False, removeSecurityProxy(snap)._allow_network)
+
 
 class TestGarboTasks(TestCaseWithFactory):
     layer = LaunchpadZopelessLayer

=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py	2017-03-27 19:28:36 +0000
+++ lib/lp/snappy/browser/snap.py	2018-01-31 14:51:33 +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 views."""
@@ -319,6 +319,7 @@
         'name',
         'private',
         'require_virtualized',
+        'allow_network',
         'auto_build',
         'store_upload',
         ])
@@ -648,7 +649,7 @@
 
     page_title = 'Administer'
 
-    field_names = ['private', 'require_virtualized']
+    field_names = ['private', 'require_virtualized', 'allow_network']
 
     def validate(self, data):
         super(SnapAdminView, self).validate(data)

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2017-10-20 13:35:42 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2018-01-31 14:51:33 +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 views."""
@@ -573,7 +573,7 @@
             user=self.person)
 
     def test_admin_snap(self):
-        # Admins can change require_virtualized and privacy.
+        # Admins can change require_virtualized, privacy, and allow_network.
         login("admin@xxxxxxxxxxxxx")
         commercial_admin = self.factory.makePerson(
             member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
@@ -581,16 +581,19 @@
         snap = self.factory.makeSnap(registrant=self.person)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
+        self.assertTrue(snap.allow_network)
 
         browser = self.getViewBrowser(snap, user=commercial_admin)
         browser.getLink("Administer snap package").click()
         browser.getControl("Require virtualized builders").selected = False
         browser.getControl("Private").selected = True
+        browser.getControl("Allow external network access").selected = False
         browser.getControl("Update snap package").click()
 
         login_person(self.person)
         self.assertFalse(snap.require_virtualized)
         self.assertTrue(snap.private)
+        self.assertFalse(snap.allow_network)
 
     def test_admin_snap_privacy_mismatch(self):
         # Cannot make snap public if it still contains private information.

=== 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-01-31 14:51:33 +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."""
@@ -581,6 +581,13 @@
         value_type=Reference(schema=IProcessor),
         readonly=False))
 
+    allow_network = exported(Bool(
+        title=_("Allow external network access"),
+        required=True, readonly=False,
+        description=_(
+            "Allow access to external network resources via a proxy.  "
+            "Resources hosted on Launchpad itself are always allowed.")))
+
 
 class ISnap(
     ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,

=== 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-01-31 14:51:33 +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
@@ -195,6 +195,21 @@
 
     private = Bool(name='private')
 
+    _allow_network = Bool(name='allow_network')
+
+    @property
+    def allow_network(self):
+        # XXX cjwatson 2018-01-31: Remove once this column has been
+        # backfilled.
+        if self._allow_network is None:
+            return True
+        else:
+            return self._allow_network
+
+    @allow_network.setter
+    def allow_network(self, value):
+        self._allow_network = value
+
     store_upload = Bool(name='store_upload', allow_none=False)
 
     store_series_id = Int(name='store_series', allow_none=True)
@@ -209,8 +224,8 @@
     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,
+                 require_virtualized=True, date_created=DEFAULT, private=False,
+                 allow_network=True, store_upload=False, store_series=None,
                  store_name=None, store_secrets=None, store_channels=None):
         """Construct a `Snap`."""
         super(Snap, self).__init__()
@@ -228,6 +243,7 @@
         self.date_created = date_created
         self.date_last_modified = date_created
         self.private = private
+        self.allow_network = allow_network
         self.store_upload = store_upload
         self.store_series = store_series
         self.store_name = store_name
@@ -660,8 +676,9 @@
             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):
+            private=False, allow_network=True, 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:
@@ -703,9 +720,10 @@
             auto_build_archive=auto_build_archive,
             auto_build_pocket=auto_build_pocket,
             require_virtualized=require_virtualized, date_created=date_created,
-            private=private, store_upload=store_upload,
-            store_series=store_series, store_name=store_name,
-            store_secrets=store_secrets, store_channels=store_channels)
+            private=private, allow_network=allow_network,
+            store_upload=store_upload, store_series=store_series,
+            store_name=store_name, store_secrets=store_secrets,
+            store_channels=store_channels)
         store.add(snap)
 
         if processors is None:

=== 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-01-31 14:51:33 +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`.
@@ -83,7 +83,7 @@
         """
         build = self.build
         args = {}
-        if config.snappy.builder_proxy_host:
+        if config.snappy.builder_proxy_host and build.snap.allow_network:
             token = yield self._requestProxyToken()
             args["proxy_url"] = (
                 "http://{username}:{password}@{host}:{port}".format(

=== 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-01-31 14:51:33 +0000
@@ -612,6 +612,7 @@
         self.assertIsNone(snap.auto_build_pocket)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
+        self.assertTrue(snap.allow_network)
 
     def test_creation_git(self):
         # The metadata entries supplied when a Snap is created for a Git
@@ -632,6 +633,7 @@
         self.assertIsNone(snap.auto_build_pocket)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
+        self.assertTrue(snap.allow_network)
 
     def test_creation_git_url(self):
         # A Snap can be backed directly by a URL for an external Git
@@ -1286,6 +1288,7 @@
             self.assertIsNone(snap["git_path"])
             self.assertIsNone(snap["git_ref_link"])
             self.assertTrue(snap["require_virtualized"])
+            self.assertTrue(snap["allow_network"])
 
     def test_new_git(self):
         # Ensure Snap creation based on a Git branch works.
@@ -1306,6 +1309,7 @@
             self.assertEqual(ref.path, snap["git_path"])
             self.assertEqual(self.getURL(ref), snap["git_ref_link"])
             self.assertTrue(snap["require_virtualized"])
+            self.assertTrue(snap["allow_network"])
 
     def test_new_private(self):
         # Ensure private Snap creation works.

=== 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-01-31 14:51:33 +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,15 @@
             ]))
 
     @defer.inlineCallbacks
+    def test_extraBuildArgs_disallow_network(self):
+        # If external network access is not allowed for the snap,
+        # _extraBuildArgs does not dispatch a proxy token.
+        job = self.makeJob(allow_network=False)
+        args = yield job._extraBuildArgs()
+        self.assertNotIn("proxy_url", args)
+        self.assertNotIn("revocation_endpoint", args)
+
+    @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-01-31 14:51:33 +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.
@@ -4650,9 +4650,9 @@
                  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,
-                 date_created=DEFAULT, private=False, store_upload=False,
-                 store_series=None, store_name=None, store_secrets=None,
-                 store_channels=None):
+                 date_created=DEFAULT, private=False, allow_network=True,
+                 store_upload=False, store_series=None, store_name=None,
+                 store_secrets=None, store_channels=None):
         """Make a new Snap."""
         if registrant is None:
             registrant = self.makePerson()
@@ -4676,9 +4676,9 @@
             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,
-            store_upload=store_upload, store_series=store_series,
-            store_name=store_name, store_secrets=store_secrets,
-            store_channels=store_channels)
+            allow_network=allow_network, store_upload=store_upload,
+            store_series=store_series, store_name=store_name,
+            store_secrets=store_secrets, store_channels=store_channels)
         if is_stale is not None:
             removeSecurityProxy(snap).is_stale = is_stale
         IStore(snap).flush()


Follow ups