← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~twom/launchpad:oci-actually-the-push-rule-models into launchpad:master

 

Tom Wardill has proposed merging ~twom/launchpad:oci-actually-the-push-rule-models into launchpad:master with ~twom/launchpad:oci-push-rule-models as a prerequisite.

Commit message:
Add OCIPushRule model

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/381035

Add the interface, model and tests for OCIPushRule, rules for pushing to an OCI registry.
Alter OCIRecipe to add a convenience lookup method.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:oci-actually-the-push-rule-models into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 70c5b72..6af7755 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -96,6 +96,7 @@ from lp.hardwaredb.interfaces.hwdb import (
     IHWSubmissionDevice,
     IHWVendorID,
     )
+from lp.oci.interfaces.ocipushrule import IOCIPushRule
 from lp.oci.interfaces.ocirecipe import IOCIRecipe
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
 from lp.registry.interfaces.commercialsubscription import (
@@ -1097,3 +1098,4 @@ patch_entry_explicit_version(IWikiName, 'beta')
 patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)
 patch_collection_property(IOCIRecipe, 'completed_builds', IOCIRecipeBuild)
 patch_collection_property(IOCIRecipe, 'pending_builds', IOCIRecipeBuild)
+patch_collection_property(IOCIRecipe, 'push_rules', IOCIPushRule)
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
index fe785ac..7cf95ed 100644
--- a/lib/lp/oci/configure.zcml
+++ b/lib/lp/oci/configure.zcml
@@ -104,4 +104,23 @@
         <allow interface="lp.services.crypto.interfaces.IEncryptedContainer"/>
     </securedutility>
 
+    <!-- OCIPushRule -->
+    <class class="lp.oci.model.ocipushrule.OCIPushRule">
+        <require
+            permission="launchpad.View"
+            interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleView
+                       lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
+        <require
+            permission="launchpad.Edit"
+            interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleEdit"
+            set_schema="lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
+    </class>
+
+    <securedutility
+        class="lp.oci.model.ocipushrule.OCIPushRuleSet"
+        provides="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet">
+        <allow
+            interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/>
+    </securedutility>
+
 </configure>
diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
new file mode 100644
index 0000000..0252748
--- /dev/null
+++ b/lib/lp/oci/interfaces/ocipushrule.py
@@ -0,0 +1,79 @@
+# Copyright 2020 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for handling credentials for OCI registry actions."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'IOCIPushRule',
+    'IOCIPushRuleSet'
+    ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+    Int,
+    TextLine,
+    )
+
+from lp import _
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
+
+
+class IOCIPushRuleView(Interface):
+    """`IOCIPushRule` methods that required launchpad.View
+    permission.
+    """
+
+    id = Int(title=_("ID"), required=True, readonly=True)
+
+
+class IOCIPushRuleEditableAttributes(Interface):
+    """`IOCIPushRule` attributes that can be edited.
+
+    These attributes need launchpad.View to see, and launchpad.Edit to change.
+    """
+
+    recipe = Reference(
+        IOCIRecipe,
+        title=_("OCI recipe"),
+        description=_("The recipe for which the rule is defined."),
+        required=True,
+        readonly=False)
+
+    registry_credentials = Reference(
+        IOCIRegistryCredentials,
+        title=_("Registry credentials"),
+        description=_("The registry credentials to use."),
+        required=True,
+        readonly=False)
+
+    image_name = TextLine(
+        title=_("Image name"),
+        description=_("The intended name of the image on the registry."),
+        required=True,
+        readonly=False)
+
+
+class IOCIPushRuleEdit(Interface):
+    """`IOCIPushRule` methods that require launchpad.Edit
+    permission.
+    """
+
+    def destroySelf():
+        """Destroy this push rule."""
+
+
+class IOCIPushRule(IOCIPushRuleEdit, IOCIPushRuleEditableAttributes,
+                   IOCIPushRuleView):
+    """A rule for pushing builds of an OCI recipe to a registry."""
+
+
+class IOCIPushRuleSet(Interface):
+    """A utility to create and access OCI Push Rules."""
+
+    def new(recipe, registry_credentials, image_name):
+        """Create an `IOCIRPushRule`."""
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index d5bef5e..8e11f69 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -136,6 +136,13 @@ class IOCIRecipeView(Interface):
         :return: `IOCIRecipeBuild`.
         """
 
+    push_rules = CollectionField(
+        title=_("Push Rules for this OCI recipe."),
+        description=_("All of the push rules for registry upload "
+                      "that apply to this recipe."),
+        # Really IOCIPushRule, patched in _schema_cirular_imports.
+        value_type=Reference(schema=Interface), readonly=True)
+
 
 class IOCIRecipeEdit(IWebhookTarget):
     """`IOCIRecipe` methods that require launchpad.Edit permission."""
diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
new file mode 100644
index 0000000..2879cbe
--- /dev/null
+++ b/lib/lp/oci/model/ocipushrule.py
@@ -0,0 +1,63 @@
+# Copyright 2020 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Registry credentials for use by an `OCIPushRule`."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'OCIPushRule',
+    'OCIPushRuleSet',
+    ]
+
+from storm.locals import (
+    Int,
+    Reference,
+    Storm,
+    Unicode,
+    )
+from zope.interface import implementer
+
+from lp.oci.interfaces.ocipushrule import (
+    IOCIPushRule,
+    IOCIPushRuleSet,
+    )
+from lp.services.database.interfaces import IStore
+
+
+@implementer(IOCIPushRule)
+class OCIPushRule(Storm):
+
+    __storm_table__ = 'OCIPushRule'
+
+    id = Int(primary=True)
+
+    recipe_id = Int(name='recipe', allow_none=False)
+    recipe = Reference(recipe_id, 'OCIRecipe.id')
+
+    registry_credentials_id = Int(
+        name='registry_credentials', allow_none=False)
+    registry_credentials = Reference(
+        registry_credentials_id, 'OCIRegistryCredentials.id')
+
+    image_name = Unicode(name="image_name", allow_none=False)
+
+    def __init__(self, recipe, registry_credentials, image_name):
+        self.recipe = recipe
+        self.registry_credentials = registry_credentials
+        self.image_name = image_name
+
+    def destroySelf(self):
+        """See `IOCIPushRule`."""
+        store = IStore(OCIPushRule)
+        store.find(
+            OCIPushRule, OCIPushRule.id == self).remove()
+
+
+@implementer(IOCIPushRuleSet)
+class OCIPushRuleSet:
+
+    def new(self, recipe, registry_credentials, image_name):
+        """See `IOCIPushRuleSet`."""
+        return OCIPushRule(recipe, registry_credentials, image_name)
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index aa3c532..7cf086f 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -46,6 +46,7 @@ from lp.oci.interfaces.ocirecipe import (
     OCIRecipeNotOwner,
     )
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.oci.model.ocipushrule import OCIPushRule
 from lp.oci.model.ocirecipebuild import OCIRecipeBuild
 from lp.registry.interfaces.person import IPersonSet
 from lp.services.database.constants import (
@@ -189,6 +190,13 @@ class OCIRecipe(Storm, WebhookTargetMixin):
         return build
 
     @property
+    def push_rules(self):
+        rules = IStore(self).find(
+            OCIPushRule,
+            OCIPushRule.recipe == self.id)
+        return rules
+
+    @property
     def _pending_states(self):
         """All the build states we consider pending (non-final)."""
         return [
diff --git a/lib/lp/oci/tests/test_ocipushrule.py b/lib/lp/oci/tests/test_ocipushrule.py
new file mode 100644
index 0000000..3fd673b
--- /dev/null
+++ b/lib/lp/oci/tests/test_ocipushrule.py
@@ -0,0 +1,55 @@
+# Copyright 2020 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for OCI registry push rules."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+from zope.component import getUtility
+
+from lp.oci.interfaces.ocipushrule import (
+    IOCIPushRule,
+    IOCIPushRuleSet,
+    )
+from lp.oci.tests.test_ociregistrycredentials import OCIConfigHelperMixin
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestOCIPushRule(OCIConfigHelperMixin, TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestOCIPushRule, self).setUp()
+        self.setConfig()
+
+    def test_implements_interface(self):
+        push_rule = self.factory.makeOCIPushRule()
+        self.assertProvides(push_rule, IOCIPushRule)
+
+
+class TestOCIPushRuleSet(OCIConfigHelperMixin, TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestOCIPushRuleSet, self).setUp()
+        self.setConfig()
+
+    def test_implements_interface(self):
+        push_rule_set = getUtility(IOCIPushRuleSet)
+        self.assertProvides(push_rule_set, IOCIPushRuleSet)
+
+    def test_new(self):
+        recipe = self.factory.makeOCIRecipe()
+        registry_credentials = self.factory.makeOCIRegistryCredentials()
+        image_name = self.factory.getUniqueUnicode()
+        push_rule = getUtility(IOCIPushRuleSet).new(
+            recipe=recipe,
+            registry_credentials=registry_credentials,
+            image_name=image_name)
+
+        self.assertEqual(push_rule.recipe, recipe)
+        self.assertEqual(push_rule.registry_credentials, registry_credentials)
+        self.assertEqual(push_rule.image_name, image_name)
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index e930ccc..53e4d34 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -5,7 +5,10 @@
 
 from __future__ import absolute_import, print_function, unicode_literals
 
+import base64
+
 from fixtures import FakeLogger
+from nacl.public import PrivateKey
 from storm.exceptions import LostObjectError
 from testtools.matchers import (
     Equals,
@@ -185,6 +188,21 @@ class TestOCIRecipe(TestCaseWithFactory):
             [fullybuilt, instacancelled], list(oci_recipe.completed_builds))
         self.assertEqual([], list(oci_recipe.pending_builds))
 
+    def test_push_rules(self):
+        self.pushConfig(
+            "oci",
+            registry_secrets_public_key=base64.b64encode(
+                bytes(PrivateKey.generate().public_key)).decode("UTF-8"))
+        oci_recipe = self.factory.makeOCIRecipe()
+        for _ in range(3):
+            self.factory.makeOCIPushRule(recipe=oci_recipe)
+        # Add some others
+        for _ in range(3):
+            self.factory.makeOCIPushRule()
+
+        for rule in oci_recipe.push_rules:
+            self.assertEqual(rule.recipe, oci_recipe)
+
 
 class TestOCIRecipeSet(TestCaseWithFactory):
 
diff --git a/lib/lp/oci/tests/test_ociregistrycredentials.py b/lib/lp/oci/tests/test_ociregistrycredentials.py
index 283ff0c..b78b900 100644
--- a/lib/lp/oci/tests/test_ociregistrycredentials.py
+++ b/lib/lp/oci/tests/test_ociregistrycredentials.py
@@ -29,12 +29,9 @@ from lp.testing import (
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
-class TestOCIRegistryCredentials(TestCaseWithFactory):
+class OCIConfigHelperMixin:
 
-    layer = LaunchpadZopelessLayer
-
-    def setUp(self):
-        super(TestOCIRegistryCredentials, self).setUp()
+    def setConfig(self):
         self.private_key = PrivateKey.generate()
         self.pushConfig(
             "oci",
@@ -45,6 +42,15 @@ class TestOCIRegistryCredentials(TestCaseWithFactory):
             registry_secrets_private_key=base64.b64encode(
                 bytes(self.private_key)).decode("UTF-8"))
 
+
+class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestOCIRegistryCredentials, self).setUp()
+        self.setConfig()
+
     def test_implements_interface(self):
         oci_credentials = getUtility(IOCIRegistryCredentialsSet).new(
             owner=self.factory.makePerson(),
@@ -77,21 +83,13 @@ class TestOCIRegistryCredentials(TestCaseWithFactory):
             }))
 
 
-class TestOCIRegistryCredentialsSet(TestCaseWithFactory):
+class TestOCIRegistryCredentialsSet(OCIConfigHelperMixin, TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer
 
     def setUp(self):
         super(TestOCIRegistryCredentialsSet, self).setUp()
-        self.private_key = PrivateKey.generate()
-        self.pushConfig(
-            "oci",
-            registry_secrets_public_key=base64.b64encode(
-                bytes(self.private_key.public_key)).decode("UTF-8"))
-        self.pushConfig(
-            "oci",
-            registry_secrets_private_key=base64.b64encode(
-                bytes(self.private_key)).decode("UTF-8"))
+        self.setConfig()
 
     def test_implements_interface(self):
         credentials_set = getUtility(IOCIRegistryCredentialsSet)
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 10aad34..a44393a 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -112,6 +112,7 @@ from lp.hardwaredb.interfaces.hwdb import (
     IHWSubmissionDevice,
     IHWVendorID,
     )
+from lp.oci.interfaces.ocipushrule import IOCIPushRule
 from lp.oci.interfaces.ocirecipe import IOCIRecipe
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
 from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
@@ -3521,3 +3522,8 @@ class ViewOCIRegistryCredentials(AuthorizationBase):
         return (
             user.isOwner(self.obj) or
             user.in_admin)
+
+
+class ViewOCIPushRule(AnonymousAuthorization):
+    """Anyone can view an `IOCIPushRule`."""
+    usedfor = IOCIPushRule
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 78f7c09..3a57790 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -157,6 +157,7 @@ from lp.hardwaredb.interfaces.hwdb import (
     IHWSubmissionDeviceSet,
     IHWSubmissionSet,
     )
+from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
 from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
 from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentialsSet
@@ -5049,6 +5050,20 @@ class BareLaunchpadObjectFactory(ObjectFactory):
             url=url,
             credentials=credentials)
 
+    def makeOCIPushRule(self, recipe=None, registry_credentials=None,
+                        image_name=None):
+        """Make a new OCIPushRule."""
+        if recipe is None:
+            recipe = self.makeOCIRecipe()
+        if registry_credentials is None:
+            registry_credentials = self.makeOCIRegistryCredentials()
+        if image_name is None:
+            image_name = self.getUniqueUnicode(u"oci-image-name")
+        return getUtility(IOCIPushRuleSet).new(
+            recipe=recipe,
+            registry_credentials=registry_credentials,
+            image_name=image_name)
+
 
 # Some factory methods return simple Python types. We don't add
 # security wrappers for them, as well as for objects created by