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

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: ee12bf4937e3521298cb00c036b61d42c7172d6b
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-actually-the-push-rule-models
Merge into: launchpad:master
Prerequisite: ~twom/launchpad:oci-push-rule-models
Diff against target: 514 lines (+316/-22)
12 files modified
lib/lp/_schema_circular_imports.py (+2/-0)
lib/lp/oci/configure.zcml (+19/-0)
lib/lp/oci/interfaces/ocipushrule.py (+79/-0)
lib/lp/oci/interfaces/ocirecipe.py (+7/-0)
lib/lp/oci/model/ocipushrule.py (+61/-0)
lib/lp/oci/model/ocirecipe.py (+8/-0)
lib/lp/oci/tests/helpers.py (+27/-0)
lib/lp/oci/tests/test_ocipushrule.py (+70/-0)
lib/lp/oci/tests/test_ocirecipe.py (+17/-0)
lib/lp/oci/tests/test_ociregistrycredentials.py (+5/-22)
lib/lp/security.py (+6/-0)
lib/lp/testing/factory.py (+15/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+381035@code.launchpad.net

Commit message

Add OCIPushRule model

Description of the change

Add the interface, model and tests for OCIPushRule, rules for pushing to an OCI registry.
Alter OCIRecipe to add a convenience lookup method.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This can't land until master has database/schema/patch-2210-08-6.sql.

review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
ee12bf4... by Tom Wardill

Fix rebase issues

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
2index a50ddd2..e74605c 100644
3--- a/lib/lp/_schema_circular_imports.py
4+++ b/lib/lp/_schema_circular_imports.py
5@@ -96,6 +96,7 @@ from lp.hardwaredb.interfaces.hwdb import (
6 IHWSubmissionDevice,
7 IHWVendorID,
8 )
9+from lp.oci.interfaces.ocipushrule import IOCIPushRule
10 from lp.oci.interfaces.ocirecipe import IOCIRecipe
11 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
12 from lp.registry.interfaces.commercialsubscription import (
13@@ -1106,3 +1107,4 @@ patch_collection_property(IOCIProject, 'series', IOCIProjectSeries)
14 patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)
15 patch_collection_property(IOCIRecipe, 'completed_builds', IOCIRecipeBuild)
16 patch_collection_property(IOCIRecipe, 'pending_builds', IOCIRecipeBuild)
17+patch_collection_property(IOCIRecipe, 'push_rules', IOCIPushRule)
18diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
19index 3246bc0..43ea399 100644
20--- a/lib/lp/oci/configure.zcml
21+++ b/lib/lp/oci/configure.zcml
22@@ -107,4 +107,23 @@
23 <allow interface="lp.services.crypto.interfaces.IEncryptedContainer"/>
24 </securedutility>
25
26+ <!-- OCIPushRule -->
27+ <class class="lp.oci.model.ocipushrule.OCIPushRule">
28+ <require
29+ permission="launchpad.View"
30+ interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleView
31+ lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
32+ <require
33+ permission="launchpad.Edit"
34+ interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleEdit"
35+ set_schema="lp.oci.interfaces.ocipushrule.IOCIPushRuleEditableAttributes" />
36+ </class>
37+
38+ <securedutility
39+ class="lp.oci.model.ocipushrule.OCIPushRuleSet"
40+ provides="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet">
41+ <allow
42+ interface="lp.oci.interfaces.ocipushrule.IOCIPushRuleSet"/>
43+ </securedutility>
44+
45 </configure>
46diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
47new file mode 100644
48index 0000000..2351edf
49--- /dev/null
50+++ b/lib/lp/oci/interfaces/ocipushrule.py
51@@ -0,0 +1,79 @@
52+# Copyright 2020 Canonical Ltd. This software is licensed under the
53+# GNU Affero General Public License version 3 (see the file LICENSE).
54+
55+"""Interfaces for handling credentials for OCI registry actions."""
56+
57+from __future__ import absolute_import, print_function, unicode_literals
58+
59+__metaclass__ = type
60+__all__ = [
61+ 'IOCIPushRule',
62+ 'IOCIPushRuleSet'
63+ ]
64+
65+from lazr.restful.fields import Reference
66+from zope.interface import Interface
67+from zope.schema import (
68+ Int,
69+ TextLine,
70+ )
71+
72+from lp import _
73+from lp.oci.interfaces.ocirecipe import IOCIRecipe
74+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
75+
76+
77+class IOCIPushRuleView(Interface):
78+ """`IOCIPushRule` methods that require launchpad.View
79+ permission.
80+ """
81+
82+ id = Int(title=_("ID"), required=True, readonly=True)
83+
84+
85+class IOCIPushRuleEditableAttributes(Interface):
86+ """`IOCIPushRule` attributes that can be edited.
87+
88+ These attributes need launchpad.View to see, and launchpad.Edit to change.
89+ """
90+
91+ recipe = Reference(
92+ IOCIRecipe,
93+ title=_("OCI recipe"),
94+ description=_("The recipe for which the rule is defined."),
95+ required=True,
96+ readonly=False)
97+
98+ registry_credentials = Reference(
99+ IOCIRegistryCredentials,
100+ title=_("Registry credentials"),
101+ description=_("The registry credentials to use."),
102+ required=True,
103+ readonly=False)
104+
105+ image_name = TextLine(
106+ title=_("Image name"),
107+ description=_("The intended name of the image on the registry."),
108+ required=True,
109+ readonly=False)
110+
111+
112+class IOCIPushRuleEdit(Interface):
113+ """`IOCIPushRule` methods that require launchpad.Edit
114+ permission.
115+ """
116+
117+ def destroySelf():
118+ """Destroy this push rule."""
119+
120+
121+class IOCIPushRule(IOCIPushRuleEdit, IOCIPushRuleEditableAttributes,
122+ IOCIPushRuleView):
123+ """A rule for pushing builds of an OCI recipe to a registry."""
124+
125+
126+class IOCIPushRuleSet(Interface):
127+ """A utility to create and access OCI Push Rules."""
128+
129+ def new(recipe, registry_credentials, image_name):
130+ """Create an `IOCIPushRule`."""
131diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
132index d5a7c92..99a0719 100644
133--- a/lib/lp/oci/interfaces/ocirecipe.py
134+++ b/lib/lp/oci/interfaces/ocirecipe.py
135@@ -140,6 +140,13 @@ class IOCIRecipeView(Interface):
136 :return: `IOCIRecipeBuild`.
137 """
138
139+ push_rules = CollectionField(
140+ title=_("Push rules for this OCI recipe."),
141+ description=_("All of the push rules for registry upload "
142+ "that apply to this recipe."),
143+ # Really IOCIPushRule, patched in _schema_cirular_imports.
144+ value_type=Reference(schema=Interface), readonly=True)
145+
146
147 class IOCIRecipeEdit(IWebhookTarget):
148 """`IOCIRecipe` methods that require launchpad.Edit permission."""
149diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
150new file mode 100644
151index 0000000..84e4a1c
152--- /dev/null
153+++ b/lib/lp/oci/model/ocipushrule.py
154@@ -0,0 +1,61 @@
155+# Copyright 2020 Canonical Ltd. This software is licensed under the
156+# GNU Affero General Public License version 3 (see the file LICENSE).
157+
158+"""Registry credentials for use by an `OCIPushRule`."""
159+
160+from __future__ import absolute_import, print_function, unicode_literals
161+
162+__metaclass__ = type
163+__all__ = [
164+ 'OCIPushRule',
165+ 'OCIPushRuleSet',
166+ ]
167+
168+from storm.locals import (
169+ Int,
170+ Reference,
171+ Storm,
172+ Unicode,
173+ )
174+from zope.interface import implementer
175+
176+from lp.oci.interfaces.ocipushrule import (
177+ IOCIPushRule,
178+ IOCIPushRuleSet,
179+ )
180+from lp.services.database.interfaces import IStore
181+
182+
183+@implementer(IOCIPushRule)
184+class OCIPushRule(Storm):
185+
186+ __storm_table__ = 'OCIPushRule'
187+
188+ id = Int(primary=True)
189+
190+ recipe_id = Int(name='recipe', allow_none=False)
191+ recipe = Reference(recipe_id, 'OCIRecipe.id')
192+
193+ registry_credentials_id = Int(
194+ name='registry_credentials', allow_none=False)
195+ registry_credentials = Reference(
196+ registry_credentials_id, 'OCIRegistryCredentials.id')
197+
198+ image_name = Unicode(name="image_name", allow_none=False)
199+
200+ def __init__(self, recipe, registry_credentials, image_name):
201+ self.recipe = recipe
202+ self.registry_credentials = registry_credentials
203+ self.image_name = image_name
204+
205+ def destroySelf(self):
206+ """See `IOCIPushRule`."""
207+ IStore(OCIPushRule).get(self.id).remove()
208+
209+
210+@implementer(IOCIPushRuleSet)
211+class OCIPushRuleSet:
212+
213+ def new(self, recipe, registry_credentials, image_name):
214+ """See `IOCIPushRuleSet`."""
215+ return OCIPushRule(recipe, registry_credentials, image_name)
216diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
217index 9a4bd8e..68f5e92 100644
218--- a/lib/lp/oci/model/ocirecipe.py
219+++ b/lib/lp/oci/model/ocirecipe.py
220@@ -46,6 +46,7 @@ from lp.oci.interfaces.ocirecipe import (
221 OCIRecipeNotOwner,
222 )
223 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
224+from lp.oci.model.ocipushrule import OCIPushRule
225 from lp.oci.model.ocirecipebuild import OCIRecipeBuild
226 from lp.registry.interfaces.person import IPersonSet
227 from lp.services.database.constants import (
228@@ -189,6 +190,13 @@ class OCIRecipe(Storm, WebhookTargetMixin):
229 return build
230
231 @property
232+ def push_rules(self):
233+ rules = IStore(self).find(
234+ OCIPushRule,
235+ OCIPushRule.recipe == self.id)
236+ return rules
237+
238+ @property
239 def _pending_states(self):
240 """All the build states we consider pending (non-final)."""
241 return [
242diff --git a/lib/lp/oci/tests/helpers.py b/lib/lp/oci/tests/helpers.py
243new file mode 100644
244index 0000000..dc0e82f
245--- /dev/null
246+++ b/lib/lp/oci/tests/helpers.py
247@@ -0,0 +1,27 @@
248+# Copyright 2020 Canonical Ltd. This software is licensed under the
249+# GNU Affero General Public License version 3 (see the file LICENSE).
250+
251+"""Helper methods and mixins for OCI tests."""
252+
253+from __future__ import absolute_import, print_function, unicode_literals
254+
255+__metaclass__ = type
256+__all__ = []
257+
258+import base64
259+
260+from nacl.public import PrivateKey
261+
262+
263+class OCIConfigHelperMixin:
264+
265+ def setConfig(self):
266+ self.private_key = PrivateKey.generate()
267+ self.pushConfig(
268+ "oci",
269+ registry_secrets_public_key=base64.b64encode(
270+ bytes(self.private_key.public_key)).decode("UTF-8"))
271+ self.pushConfig(
272+ "oci",
273+ registry_secrets_private_key=base64.b64encode(
274+ bytes(self.private_key)).decode("UTF-8"))
275diff --git a/lib/lp/oci/tests/test_ocipushrule.py b/lib/lp/oci/tests/test_ocipushrule.py
276new file mode 100644
277index 0000000..90ddd96
278--- /dev/null
279+++ b/lib/lp/oci/tests/test_ocipushrule.py
280@@ -0,0 +1,70 @@
281+# Copyright 2020 Canonical Ltd. This software is licensed under the
282+# GNU Affero General Public License version 3 (see the file LICENSE).
283+
284+"""Tests for OCI registry push rules."""
285+
286+from __future__ import absolute_import, print_function, unicode_literals
287+
288+from testtools.matchers import MatchesStructure
289+from zope.component import getUtility
290+
291+from lp.oci.interfaces.ocipushrule import (
292+ IOCIPushRule,
293+ IOCIPushRuleSet,
294+ )
295+from lp.oci.tests.helpers import OCIConfigHelperMixin
296+from lp.testing import (
297+ person_logged_in,
298+ TestCaseWithFactory,
299+ )
300+from lp.testing.layers import LaunchpadZopelessLayer
301+
302+
303+class TestOCIPushRule(OCIConfigHelperMixin, TestCaseWithFactory):
304+
305+ layer = LaunchpadZopelessLayer
306+
307+ def setUp(self):
308+ super(TestOCIPushRule, self).setUp()
309+ self.setConfig()
310+
311+ def test_implements_interface(self):
312+ push_rule = self.factory.makeOCIPushRule()
313+ self.assertProvides(push_rule, IOCIPushRule)
314+
315+ def test_change_attribute(self):
316+ push_rule = self.factory.makeOCIPushRule()
317+ with person_logged_in(push_rule.recipe.owner):
318+ push_rule.image_name = 'new image name'
319+
320+ found_rule = push_rule.recipe.push_rules[0]
321+ self.assertEqual(found_rule.image_name, 'new image name')
322+
323+
324+class TestOCIPushRuleSet(OCIConfigHelperMixin, TestCaseWithFactory):
325+
326+ layer = LaunchpadZopelessLayer
327+
328+ def setUp(self):
329+ super(TestOCIPushRuleSet, self).setUp()
330+ self.setConfig()
331+
332+ def test_implements_interface(self):
333+ push_rule_set = getUtility(IOCIPushRuleSet)
334+ self.assertProvides(push_rule_set, IOCIPushRuleSet)
335+
336+ def test_new(self):
337+ recipe = self.factory.makeOCIRecipe()
338+ registry_credentials = self.factory.makeOCIRegistryCredentials()
339+ image_name = self.factory.getUniqueUnicode()
340+ push_rule = getUtility(IOCIPushRuleSet).new(
341+ recipe=recipe,
342+ registry_credentials=registry_credentials,
343+ image_name=image_name)
344+
345+ self.assertThat(
346+ push_rule,
347+ MatchesStructure.byEquality(
348+ recipe=recipe,
349+ registry_credentials=registry_credentials,
350+ image_name=image_name))
351diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
352index 379d0ce..3c60e7e 100644
353--- a/lib/lp/oci/tests/test_ocirecipe.py
354+++ b/lib/lp/oci/tests/test_ocirecipe.py
355@@ -5,10 +5,12 @@
356
357 from __future__ import absolute_import, print_function, unicode_literals
358
359+import base64
360 import json
361
362 from fixtures import FakeLogger
363 from six import string_types
364+from nacl.public import PrivateKey
365 from storm.exceptions import LostObjectError
366 from testtools.matchers import (
367 ContainsDict,
368@@ -192,6 +194,21 @@ class TestOCIRecipe(TestCaseWithFactory):
369 [fullybuilt, instacancelled], list(oci_recipe.completed_builds))
370 self.assertEqual([], list(oci_recipe.pending_builds))
371
372+ def test_push_rules(self):
373+ self.pushConfig(
374+ "oci",
375+ registry_secrets_public_key=base64.b64encode(
376+ bytes(PrivateKey.generate().public_key)).decode("UTF-8"))
377+ oci_recipe = self.factory.makeOCIRecipe()
378+ for _ in range(3):
379+ self.factory.makeOCIPushRule(recipe=oci_recipe)
380+ # Add some others
381+ for _ in range(3):
382+ self.factory.makeOCIPushRule()
383+
384+ for rule in oci_recipe.push_rules:
385+ self.assertEqual(rule.recipe, oci_recipe)
386+
387
388 class TestOCIRecipeSet(TestCaseWithFactory):
389
390diff --git a/lib/lp/oci/tests/test_ociregistrycredentials.py b/lib/lp/oci/tests/test_ociregistrycredentials.py
391index 283ff0c..fba4b1a 100644
392--- a/lib/lp/oci/tests/test_ociregistrycredentials.py
393+++ b/lib/lp/oci/tests/test_ociregistrycredentials.py
394@@ -5,10 +5,8 @@
395
396 from __future__ import absolute_import, print_function, unicode_literals
397
398-import base64
399 import json
400
401-from nacl.public import PrivateKey
402 from testtools.matchers import (
403 AfterPreprocessing,
404 Equals,
405@@ -21,6 +19,7 @@ from lp.oci.interfaces.ociregistrycredentials import (
406 IOCIRegistryCredentials,
407 IOCIRegistryCredentialsSet,
408 )
409+from lp.oci.tests.helpers import OCIConfigHelperMixin
410 from lp.services.crypto.interfaces import IEncryptedContainer
411 from lp.testing import (
412 person_logged_in,
413@@ -29,21 +28,13 @@ from lp.testing import (
414 from lp.testing.layers import LaunchpadZopelessLayer
415
416
417-class TestOCIRegistryCredentials(TestCaseWithFactory):
418+class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
419
420 layer = LaunchpadZopelessLayer
421
422 def setUp(self):
423 super(TestOCIRegistryCredentials, self).setUp()
424- self.private_key = PrivateKey.generate()
425- self.pushConfig(
426- "oci",
427- registry_secrets_public_key=base64.b64encode(
428- bytes(self.private_key.public_key)).decode("UTF-8"))
429- self.pushConfig(
430- "oci",
431- registry_secrets_private_key=base64.b64encode(
432- bytes(self.private_key)).decode("UTF-8"))
433+ self.setConfig()
434
435 def test_implements_interface(self):
436 oci_credentials = getUtility(IOCIRegistryCredentialsSet).new(
437@@ -77,21 +68,13 @@ class TestOCIRegistryCredentials(TestCaseWithFactory):
438 }))
439
440
441-class TestOCIRegistryCredentialsSet(TestCaseWithFactory):
442+class TestOCIRegistryCredentialsSet(OCIConfigHelperMixin, TestCaseWithFactory):
443
444 layer = LaunchpadZopelessLayer
445
446 def setUp(self):
447 super(TestOCIRegistryCredentialsSet, self).setUp()
448- self.private_key = PrivateKey.generate()
449- self.pushConfig(
450- "oci",
451- registry_secrets_public_key=base64.b64encode(
452- bytes(self.private_key.public_key)).decode("UTF-8"))
453- self.pushConfig(
454- "oci",
455- registry_secrets_private_key=base64.b64encode(
456- bytes(self.private_key)).decode("UTF-8"))
457+ self.setConfig()
458
459 def test_implements_interface(self):
460 credentials_set = getUtility(IOCIRegistryCredentialsSet)
461diff --git a/lib/lp/security.py b/lib/lp/security.py
462index 10aad34..a44393a 100644
463--- a/lib/lp/security.py
464+++ b/lib/lp/security.py
465@@ -112,6 +112,7 @@ from lp.hardwaredb.interfaces.hwdb import (
466 IHWSubmissionDevice,
467 IHWVendorID,
468 )
469+from lp.oci.interfaces.ocipushrule import IOCIPushRule
470 from lp.oci.interfaces.ocirecipe import IOCIRecipe
471 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
472 from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
473@@ -3521,3 +3522,8 @@ class ViewOCIRegistryCredentials(AuthorizationBase):
474 return (
475 user.isOwner(self.obj) or
476 user.in_admin)
477+
478+
479+class ViewOCIPushRule(AnonymousAuthorization):
480+ """Anyone can view an `IOCIPushRule`."""
481+ usedfor = IOCIPushRule
482diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
483index c66175d..8e0f1c1 100644
484--- a/lib/lp/testing/factory.py
485+++ b/lib/lp/testing/factory.py
486@@ -157,6 +157,7 @@ from lp.hardwaredb.interfaces.hwdb import (
487 IHWSubmissionDeviceSet,
488 IHWSubmissionSet,
489 )
490+from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
491 from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
492 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
493 from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentialsSet
494@@ -5051,6 +5052,20 @@ class BareLaunchpadObjectFactory(ObjectFactory):
495 url=url,
496 credentials=credentials)
497
498+ def makeOCIPushRule(self, recipe=None, registry_credentials=None,
499+ image_name=None):
500+ """Make a new OCIPushRule."""
501+ if recipe is None:
502+ recipe = self.makeOCIRecipe()
503+ if registry_credentials is None:
504+ registry_credentials = self.makeOCIRegistryCredentials()
505+ if image_name is None:
506+ image_name = self.getUniqueUnicode(u"oci-image-name")
507+ return getUtility(IOCIPushRuleSet).new(
508+ recipe=recipe,
509+ registry_credentials=registry_credentials,
510+ image_name=image_name)
511+
512
513 # Some factory methods return simple Python types. We don't add
514 # security wrappers for them, as well as for objects created by

Subscribers

People subscribed via source and target branches

to status/vote changes: