launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25807
[Merge] ~twom/launchpad:oci-policy-push-tags-to-the-limit into launchpad:master
Tom Wardill has proposed merging ~twom/launchpad:oci-policy-push-tags-to-the-limit into launchpad:master with ~twom/launchpad:db-oci-policy-push-tags-to-the-limit as a prerequisite.
Commit message:
Add model for OCIImageTag
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/394959
Add a model, interface, and supporting configuration for OCI Image Tag.
Reference to OCIPushRule, with a ReferenceSet back to OCIImageTag.
Can't land until after https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/394958
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:oci-policy-push-tags-to-the-limit into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 11aba6f..5355ce5 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -242,6 +242,7 @@ public.oauthaccesstoken = SELECT, INSERT, UPDATE, DELETE
public.oauthconsumer = SELECT, INSERT
public.oauthrequesttoken = SELECT, INSERT, UPDATE, DELETE
public.ocifile = SELECT, INSERT, UPDATE, DELETE
+public.ociimagetag = SELECT, INSERT, UPDATE, DELETE
public.ociproject = SELECT, INSERT, UPDATE, DELETE
public.ociprojectname = SELECT, INSERT, UPDATE
public.ociprojectseries = SELECT, INSERT, UPDATE, DELETE
@@ -998,6 +999,7 @@ public.livefs = SELECT
public.livefsbuild = SELECT, UPDATE
public.livefsfile = SELECT
public.ocifile = SELECT, UPDATE
+public.ociimagetag = SELECT
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ocipushrule = SELECT
@@ -1441,6 +1443,7 @@ public.messagechunk = SELECT, INSERT
public.milestone = SELECT
public.milestonetag = SELECT
public.ocifile = SELECT, INSERT
+public.ociimagetag = SELECT
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ociprojectseries = SELECT
@@ -2734,6 +2737,7 @@ public.ocirecipebuild = SELECT, INSERT, UPDATE
public.ocirecipebuildjob = SELECT, UPDATE
public.ocirecipejob = SELECT, UPDATE, INSERT
public.ocifile = SELECT
+public.ociimagetag = SELECT
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ociprojectseries = SELECT
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 6e2cbfc..0f679e6 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -84,6 +84,8 @@ from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuild,
)
+from lp.oci.interfaces.ociimagetag import IOCIImageTag
+from lp.oci.interfaces.ocipushrule import IOCIPushRule
from lp.registry.interfaces.commercialsubscription import (
ICommercialSubscription,
)
@@ -688,6 +690,9 @@ patch_collection_property(
patch_collection_property(
IHasSpecifications, 'api_valid_specifications', ISpecification)
+# IOCIPushRule
+patch_collection_property(IOCIPushRule, 'tags', IOCIImageTag)
+
###
#
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
index 1f7a6c7..9f2871a 100644
--- a/lib/lp/oci/configure.zcml
+++ b/lib/lp/oci/configure.zcml
@@ -155,6 +155,23 @@
interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/>
</securedutility>
+ <!-- OCIImageTag -->
+ <class class="lp.oci.model.ociimagetag.OCIImageTag">
+ <require
+ permission="launchpad.View"
+ interface="lp.oci.interfaces.ociimagetag.IOCIImageTagView" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.oci.interfaces.ociimagetag.IOCIImageTagEdit" />
+ </class>
+
+ <securedutility
+ class="lp.oci.model.ociimagetag.OCIImageTagSet"
+ provides="lp.oci.interfaces.ociimagetag.IOCIImageTagSet">
+ <allow
+ interface="lp.oci.interfaces.ociimagetag.IOCIImageTagSet"/>
+ </securedutility>
+
<!-- OCI related jobs -->
<securedutility
component="lp.oci.model.ocirecipebuildjob.OCIRegistryUploadJob"
diff --git a/lib/lp/oci/interfaces/ociimagetag.py b/lib/lp/oci/interfaces/ociimagetag.py
new file mode 100644
index 0000000..fe7220d
--- /dev/null
+++ b/lib/lp/oci/interfaces/ociimagetag.py
@@ -0,0 +1,71 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Registry tags for OCI Images."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IOCIImageTag',
+ 'IOCIImageTagSet',
+ 'OCIImageTagAlreadyExists',
+ ]
+
+from lazr.restful.declarations import error_status
+from lazr.restful.fields import Reference
+from six.moves import http_client
+from zope.interface import Interface
+from zope.schema import (
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.oci.interfaces.ocipushrule import IOCIPushRule
+
+
+@error_status(http_client.CONFLICT)
+class OCIImageTagAlreadyExists(Exception):
+ """A OCIPushRule already has a tag with the same details."""
+
+ def __init__(self):
+ super(OCIImageTagAlreadyExists).__init__(
+ "The tag already exists for this push rule.")
+
+
+class IOCIImageTagView(Interface):
+ """`IOCIImageTag` methods that require Launchpad.View permission."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+ tag = TextLine(
+ title=_("Tag"),
+ description=_("The tag to apply to an image on upload."),
+ required=True,
+ readonly=False)
+ push_rule = Reference(
+ IOCIPushRule,
+ title=_("Push Rule"),
+ description=_("The Push Rule that uses this tag."),
+ required=True,
+ readonly=True)
+
+
+class IOCIImageTagEdit(Interface):
+ """`IOCIImageTag` methods that require launchpad.Edit
+ permission.
+ """
+
+ def destroySelf():
+ """Destroy this image tag."""
+
+
+class IOCIImageTag(IOCIImageTagView, IOCIImageTagEdit):
+ """A tag to be added to an OCI Image on push to a registry."""
+
+
+class IOCIImageTagSet(Interface):
+ """A utility to create and access OCI Image tags."""
+
+ def new(push_rule, tag):
+ """Create an `IOCIImageTag`."""
diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
index 67db23e..b1d97e4 100644
--- a/lib/lp/oci/interfaces/ocipushrule.py
+++ b/lib/lp/oci/interfaces/ocipushrule.py
@@ -22,7 +22,10 @@ from lazr.restful.declarations import (
operation_for_version,
operation_parameters,
)
-from lazr.restful.fields import Reference
+from lazr.restful.fields import (
+ Reference,
+ CollectionField,
+ )
from six.moves import http_client
from zope.interface import Interface
from zope.schema import (
@@ -37,7 +40,7 @@ from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
@error_status(http_client.CONFLICT)
class OCIPushRuleAlreadyExists(Exception):
- """A new OCIPushRuleAlreadyExists was added with the
+ """A new OCIPushRule was added with the
same details as an existing one.
"""
@@ -102,6 +105,13 @@ class IOCIPushRuleEditableAttributes(Interface):
def setNewImageName(image_name):
"""Set the new image name, checking for uniqueness."""
+ tags = CollectionField(
+ title=_("Image tags"),
+ description=_(
+ "The list of tags that will be applied to an image on push."),
+ value_type=Reference(schema=Interface), # IOCIImageTag
+ required=False, default=[], readonly=True)
+
class IOCIPushRuleEdit(Interface):
"""`IOCIPushRule` methods that require launchpad.Edit
diff --git a/lib/lp/oci/model/ociimagetag.py b/lib/lp/oci/model/ociimagetag.py
new file mode 100644
index 0000000..6ae7802
--- /dev/null
+++ b/lib/lp/oci/model/ociimagetag.py
@@ -0,0 +1,59 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Registry tags for an OCI Image, used by an `OCIPushRule`."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OCIImageTag',
+ 'OCIImageTagSet'
+]
+
+from storm.locals import (
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.oci.interfaces.ociimagetag import (
+ IOCIImageTag,
+ IOCIImageTagSet,
+ OCIImageTagAlreadyExists,
+ )
+from lp.services.database.interfaces import IStore
+
+
+@implementer(IOCIImageTag)
+class OCIImageTag(Storm):
+
+ __storm_table__ = 'OCIImageTag'
+
+ id = Int(primary=True)
+ tag = Unicode(name='tag', allow_none=False)
+ push_rule_id = Int(name="push_rule", allow_none=False)
+ push_rule = Reference(push_rule_id, 'OCIPushRule.id')
+
+ def __init__(self, push_rule, tag):
+ self.push_rule = push_rule
+ self.tag = tag
+
+ def destroySelf(self):
+ """See `IOCIImageTag`."""
+ IStore(OCIImageTag).remove(self)
+
+
+@implementer(IOCIImageTagSet)
+class OCIImageTagSet:
+
+ def new(self, push_rule, tag):
+ """See `IOCIImageTagSet`."""
+ for existing in push_rule.tags:
+ if tag == existing.tag:
+ raise OCIImageTagAlreadyExists()
+ tag = OCIImageTag(push_rule, tag)
+ IStore(OCIImageTag).add(tag)
+ return tag
diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
index 24d5e27..df950d6 100644
--- a/lib/lp/oci/model/ocipushrule.py
+++ b/lib/lp/oci/model/ocipushrule.py
@@ -5,6 +5,8 @@
from __future__ import absolute_import, print_function, unicode_literals
+from storm.references import ReferenceSet
+
__metaclass__ = type
__all__ = [
'OCIPushRule',
@@ -44,6 +46,9 @@ class OCIPushRule(Storm):
image_name = Unicode(name="image_name", allow_none=False)
+ tags = ReferenceSet(
+ 'id', 'OCIImageTag.push_rule_id', order_by='OCIImageTag.id')
+
@property
def registry_url(self):
return self.registry_credentials.url
@@ -69,6 +74,8 @@ class OCIPushRule(Storm):
def destroySelf(self):
"""See `IOCIPushRule`."""
+ for tag in self.tags:
+ tag.removeSelf()
IStore(OCIPushRule).remove(self)
diff --git a/lib/lp/oci/tests/test_ociimagetag.py b/lib/lp/oci/tests/test_ociimagetag.py
new file mode 100644
index 0000000..5b9dbe4
--- /dev/null
+++ b/lib/lp/oci/tests/test_ociimagetag.py
@@ -0,0 +1,77 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for OCI Image tags."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+
+__metaclass__ = type
+
+from storm.store import Store
+from testtools.matchers import MatchesStructure
+from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
+
+from lp.oci.interfaces.ociimagetag import (
+ IOCIImageTag,
+ IOCIImageTagSet,
+ )
+from lp.oci.model.ociimagetag import OCIImageTag
+from lp.oci.tests.helpers import OCIConfigHelperMixin
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestOCIImageTag(OCIConfigHelperMixin, TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIImageTag, self).setUp()
+ self.setConfig()
+
+ def test_implements_interface(self):
+ image_tag = self.factory.makeOCIImageTag()
+ with person_logged_in(image_tag.push_rule.recipe.owner):
+ self.assertProvides(image_tag, IOCIImageTag)
+
+ def test_destroySelf(self):
+ image_tag = self.factory.makeOCIImageTag()
+ store = Store.of(image_tag)
+ self.assertEqual(1, store.find(OCIImageTag).count())
+ # Launchpad.Edit required
+ self.assertRaises(Unauthorized, getattr, image_tag, 'destroySelf')
+ # Launchpad.Edit is the owner of the containing recipe
+ with person_logged_in(image_tag.push_rule.recipe.owner):
+ image_tag.destroySelf()
+ self.assertEqual(0, store.find(OCIImageTag).count())
+
+
+class TestOCIImageTagSet(OCIConfigHelperMixin, TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIImageTagSet, self).setUp()
+ self.setConfig()
+
+ def test_implements_interface(self):
+ image_tag_set = getUtility(IOCIImageTagSet)
+ self.assertProvides(image_tag_set, IOCIImageTagSet)
+
+ def test_new(self):
+ push_rule = self.factory.makeOCIPushRule()
+ tag = self.factory.getUniqueUnicode()
+ image_tag = getUtility(IOCIImageTagSet).new(
+ push_rule=push_rule,
+ tag=tag)
+
+ self.assertThat(
+ image_tag,
+ MatchesStructure.byEquality(
+ push_rule=push_rule,
+ tag=tag))
diff --git a/lib/lp/oci/tests/test_ocipushrule.py b/lib/lp/oci/tests/test_ocipushrule.py
index 4c6db56..31798d6 100644
--- a/lib/lp/oci/tests/test_ocipushrule.py
+++ b/lib/lp/oci/tests/test_ocipushrule.py
@@ -71,6 +71,14 @@ class TestOCIPushRule(OCIConfigHelperMixin, TestCaseWithFactory):
# Avoid trying to flush the incomplete object on cleanUp.
Store.of(owner).rollback()
+ def test_tags_exist(self):
+ push_rule = self.factory.makeOCIPushRule()
+ first_tag = self.factory.makeOCIImageTag(push_rule=push_rule)
+ second_tag = self.factory.makeOCIImageTag(push_rule=push_rule)
+ tag_names = [x.tag for x in push_rule.tags]
+ self.assertIn(first_tag.tag, tag_names)
+ self.assertIn(second_tag.tag, tag_names)
+
class TestOCIPushRuleSet(OCIConfigHelperMixin, TestCaseWithFactory):
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 459ab8c..f4c7f6e 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -101,6 +101,7 @@ from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuild,
)
+from lp.oci.interfaces.ociimagetag import IOCIImageTag
from lp.oci.interfaces.ocipushrule import IOCIPushRule
from lp.oci.interfaces.ocirecipe import (
IOCIRecipe,
@@ -3503,7 +3504,7 @@ class ViewOCIPushRule(AnonymousAuthorization):
usedfor = IOCIPushRule
-class OCIPushRuleEdit(AuthorizationBase):
+class EditOCIPushRule(AuthorizationBase):
permission = 'launchpad.Edit'
usedfor = IOCIPushRule
@@ -3511,3 +3512,19 @@ class OCIPushRuleEdit(AuthorizationBase):
return (
user.isOwner(self.obj.recipe) or
user.in_commercial_admin or user.in_admin)
+
+
+class ViewOCIImageTag(AnonymousAuthorization):
+ """Anyone can view an `IOCIImageTag`."""
+ usedfor = IOCIImageTag
+
+
+class EditOCIImageTag(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IOCIImageTag
+
+ def checkAuthenticated(self, user):
+ auth_push_rule = EditOCIPushRule(self.obj.push_rule)
+ if auth_push_rule.checkAuthenticated(user):
+ return True
+ return super(EditOCIImageTag, self).checkAuthenticated(user)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 38b9ffb..c86237e 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -42,6 +42,8 @@ import types
import uuid
import warnings
+from lp.oci.interfaces.ociimagetag import IOCIImageTag
+
from breezy.plugins.builder.recipe import BaseRecipeBranch
from breezy.revision import Revision as BzrRevision
from cryptography.utils import int_to_bytes
@@ -150,6 +152,7 @@ from lp.code.model.diff import (
Diff,
PreviewDiff,
)
+from lp.oci.interfaces.ociimagetag import IOCIImageTagSet
from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
@@ -5093,6 +5096,16 @@ class BareLaunchpadObjectFactory(ObjectFactory):
registry_credentials=registry_credentials,
image_name=image_name)
+ def makeOCIImageTag(self, push_rule=None, tag=None):
+ """Make a new OCIImageTag."""
+ if push_rule is None:
+ push_rule = self.makeOCIPushRule()
+ if tag is None:
+ tag = self.getUniqueUnicode("tag-")
+ return getUtility(IOCIImageTagSet).new(
+ push_rule=push_rule,
+ tag=tag)
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by