Merge ~pappacena/launchpad:oci-api-create-recipe into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: ff742c693e21d53a35779f1aad946ed24df2ea8d
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:oci-api-create-recipe
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:oci-project-api
Diff against target: 842 lines (+308/-47)
15 files modified
lib/lp/_schema_circular_imports.py (+1/-0)
lib/lp/oci/browser/ocirecipe.py (+9/-1)
lib/lp/oci/browser/tests/test_ocirecipe.py (+23/-0)
lib/lp/oci/browser/tests/test_ocirecipebuild.py (+7/-0)
lib/lp/oci/interfaces/ocirecipe.py (+13/-1)
lib/lp/oci/model/ocirecipe.py (+10/-3)
lib/lp/oci/tests/helpers.py (+5/-0)
lib/lp/oci/tests/test_ocirecipe.py (+135/-32)
lib/lp/oci/tests/test_ocirecipebuild.py (+11/-2)
lib/lp/oci/tests/test_ocirecipebuildbehaviour.py (+13/-2)
lib/lp/registry/configure.zcml (+3/-0)
lib/lp/registry/interfaces/ociproject.py (+49/-2)
lib/lp/registry/model/ociproject.py (+16/-0)
lib/lp/registry/tests/test_personmerge.py (+7/-2)
lib/lp/services/webhooks/tests/test_job.py (+6/-2)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+381065@code.launchpad.net

Commit message

API operation to create a new OCIRecipe from an existing OCIProject.

The feature is only enabled if we turn on the 'oci.recipe.create.enabled' feature flag.

Description of the change

This MP includes code from https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/380909. So, it's important to hold merges until the previous one is accepted and merged.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) :
4171065... by Thiago F. Pappacena

Merge branch 'master' into oci-api-create-recipe

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
719b074... by Thiago F. Pappacena

Refactoring tests

222602a... by Thiago F. Pappacena

Removing unused parameter

d14a4c6... by Thiago F. Pappacena

Moving OCI_RECIPE_ALLOW_CREATE feature flag to OCIRecipe.__init__ and its view initializer

75e9047... by Thiago F. Pappacena

Renaming IOCIProjectPublicActions interface to IOCIProjectLegitimate

c979846... by Thiago F. Pappacena

OCIRecipe owner should be provided on creation when using API

5bc0691... by Thiago F. Pappacena

Adding build_daily on OCI Recipe creation API

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

cjwatson, it took a while to adjust all the tests that needed to `OCIRecipe.__init__` or `OCIRecipeAddView.initialize`, and those adjustments made the diff bigger, although it's just setting the correct feature flag where appropriate.

Anyway, I think it covers all requested changes. Let me know if you want to do another round of review.

d227d91... by Thiago F. Pappacena

Fixing tests to use feature flag

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Ah, I'm actually missing one of the requests. I'll push in some minutes.

fd4fa2e... by Thiago F. Pappacena

Actually using build_daily on OCIRecipe's create UI

a258de0... by Thiago F. Pappacena

Merge branch 'master' into oci-api-create-recipe

efc1f7f... by Thiago F. Pappacena

Merge branch 'master' into oci-api-create-recipe

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Now I think it's done, and ready for another round of review.

Sorry for the long diff, but most of the new changes are just adding the FeatureFixture on old tests.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
ff742c6... by Thiago F. Pappacena

Minor refactoring and adjusting constraing on OCIProject.newRecipe

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Thanks for the review. All requested changes were made. I'll top-approve this MP now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index dd4e9d4..37369b4 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -1103,6 +1103,7 @@ patch_entry_explicit_version(IWikiName, 'beta')
11031103
1104# IOCIProject1104# IOCIProject
1105patch_collection_property(IOCIProject, 'series', IOCIProjectSeries)1105patch_collection_property(IOCIProject, 'series', IOCIProjectSeries)
1106patch_entry_return_type(IOCIProject, 'newRecipe', IOCIRecipe)
11061107
1107# IOCIRecipe1108# IOCIRecipe
1108patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)1109patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 7d044f5..10e9d4b 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -35,7 +35,9 @@ from lp.oci.interfaces.ocirecipe import (
35 IOCIRecipe,35 IOCIRecipe,
36 IOCIRecipeSet,36 IOCIRecipeSet,
37 NoSuchOCIRecipe,37 NoSuchOCIRecipe,
38 OCI_RECIPE_ALLOW_CREATE,
38 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,39 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
40 OCIRecipeFeatureDisabled,
39 )41 )
40from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet42from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
41from lp.services.features import getFeatureFlag43from lp.services.features import getFeatureFlag
@@ -174,6 +176,11 @@ class OCIRecipeAddView(LaunchpadFormView):
174 )176 )
175 custom_widget_git_ref = GitRefWidget177 custom_widget_git_ref = GitRefWidget
176178
179 def initialize(self):
180 super(OCIRecipeAddView, self).initialize()
181 if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
182 raise OCIRecipeFeatureDisabled()
183
177 @property184 @property
178 def cancel_url(self):185 def cancel_url(self):
179 """See `LaunchpadFormView`."""186 """See `LaunchpadFormView`."""
@@ -205,7 +212,8 @@ class OCIRecipeAddView(LaunchpadFormView):
205 recipe = getUtility(IOCIRecipeSet).new(212 recipe = getUtility(IOCIRecipeSet).new(
206 name=data["name"], registrant=self.user, owner=data["owner"],213 name=data["name"], registrant=self.user, owner=data["owner"],
207 oci_project=self.context, git_ref=data["git_ref"],214 oci_project=self.context, git_ref=data["git_ref"],
208 build_file=data["build_file"], description=data["description"])215 build_file=data["build_file"], description=data["description"],
216 build_daily=data["build_daily"])
209 self.next_url = canonical_url(recipe)217 self.next_url = canonical_url(recipe)
210218
211219
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 540649f..bd87975 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -29,7 +29,9 @@ from lp.oci.browser.ocirecipe import (
29 OCIRecipeEditView,29 OCIRecipeEditView,
30 OCIRecipeView,30 OCIRecipeView,
31 )31 )
32from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
32from lp.services.database.constants import UTC_NOW33from lp.services.database.constants import UTC_NOW
34from lp.services.features.testing import FeatureFixture
33from lp.services.propertycache import get_property_cache35from lp.services.propertycache import get_property_cache
34from lp.services.webapp import canonical_url36from lp.services.webapp import canonical_url
35from lp.services.webapp.servers import LaunchpadTestRequest37from lp.services.webapp.servers import LaunchpadTestRequest
@@ -62,6 +64,10 @@ class TestOCIRecipeNavigation(TestCaseWithFactory):
6264
63 layer = DatabaseFunctionalLayer65 layer = DatabaseFunctionalLayer
6466
67 def setUp(self):
68 super(TestOCIRecipeNavigation, self).setUp()
69 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
70
65 def test_canonical_url(self):71 def test_canonical_url(self):
66 owner = self.factory.makePerson(name="person")72 owner = self.factory.makePerson(name="person")
67 distribution = self.factory.makeDistribution(name="distro")73 distribution = self.factory.makeDistribution(name="distro")
@@ -96,6 +102,10 @@ class BaseTestOCIRecipeView(BrowserTestCase):
96102
97class TestOCIRecipeAddView(BaseTestOCIRecipeView):103class TestOCIRecipeAddView(BaseTestOCIRecipeView):
98104
105 def setUp(self):
106 super(TestOCIRecipeAddView, self).setUp()
107 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
108
99 def test_create_new_recipe_not_logged_in(self):109 def test_create_new_recipe_not_logged_in(self):
100 oci_project = self.factory.makeOCIProject()110 oci_project = self.factory.makeOCIProject()
101 self.assertRaises(111 self.assertRaises(
@@ -151,6 +161,10 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
151161
152class TestOCIRecipeAdminView(BaseTestOCIRecipeView):162class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
153163
164 def setUp(self):
165 super(TestOCIRecipeAdminView, self).setUp()
166 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
167
154 def test_unauthorized(self):168 def test_unauthorized(self):
155 # A non-admin user cannot administer an OCI recipe.169 # A non-admin user cannot administer an OCI recipe.
156 login_person(self.person)170 login_person(self.person)
@@ -199,6 +213,10 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
199213
200class TestOCIRecipeEditView(BaseTestOCIRecipeView):214class TestOCIRecipeEditView(BaseTestOCIRecipeView):
201215
216 def setUp(self):
217 super(TestOCIRecipeEditView, self).setUp()
218 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
219
202 def test_edit_recipe(self):220 def test_edit_recipe(self):
203 oci_project = self.factory.makeOCIProject()221 oci_project = self.factory.makeOCIProject()
204 oci_project_display = oci_project.display_name222 oci_project_display = oci_project.display_name
@@ -275,6 +293,10 @@ class TestOCIRecipeEditView(BaseTestOCIRecipeView):
275293
276class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):294class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):
277295
296 def setUp(self):
297 super(TestOCIRecipeDeleteView, self).setUp()
298 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
299
278 def test_unauthorized(self):300 def test_unauthorized(self):
279 # A user without edit access cannot delete an OCI recipe.301 # A user without edit access cannot delete an OCI recipe.
280 recipe = self.factory.makeOCIRecipe(302 recipe = self.factory.makeOCIRecipe(
@@ -326,6 +348,7 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
326 distroseries=self.distroseries, architecturetag="i386",348 distroseries=self.distroseries, architecturetag="i386",
327 processor=processor)349 processor=processor)
328 self.factory.makeBuilder(virtualized=True)350 self.factory.makeBuilder(virtualized=True)
351 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
329352
330 def makeOCIRecipe(self, oci_project=None, **kwargs):353 def makeOCIRecipe(self, oci_project=None, **kwargs):
331 if oci_project is None:354 if oci_project is None:
diff --git a/lib/lp/oci/browser/tests/test_ocirecipebuild.py b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
index ed3cb9b..72f29e9 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
@@ -13,6 +13,8 @@ from storm.locals import Store
13from testtools.matchers import StartsWith13from testtools.matchers import StartsWith
1414
15from lp.buildmaster.enums import BuildStatus15from lp.buildmaster.enums import BuildStatus
16from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
17from lp.services.features.testing import FeatureFixture
16from lp.services.webapp import canonical_url18from lp.services.webapp import canonical_url
17from lp.testing import (19from lp.testing import (
18 BrowserTestCase,20 BrowserTestCase,
@@ -29,6 +31,10 @@ class TestCanonicalUrlForOCIRecipeBuild(TestCaseWithFactory):
2931
30 layer = DatabaseFunctionalLayer32 layer = DatabaseFunctionalLayer
3133
34 def setUp(self):
35 super(TestCanonicalUrlForOCIRecipeBuild, self).setUp()
36 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
37
32 def test_canonical_url(self):38 def test_canonical_url(self):
33 owner = self.factory.makePerson(name="person")39 owner = self.factory.makePerson(name="person")
34 distribution = self.factory.makeDistribution(name="distro")40 distribution = self.factory.makeDistribution(name="distro")
@@ -51,6 +57,7 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
5157
52 def setUp(self):58 def setUp(self):
53 super(TestOCIRecipeBuildOperations, self).setUp()59 super(TestOCIRecipeBuildOperations, self).setUp()
60 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
54 self.build = self.factory.makeOCIRecipeBuild()61 self.build = self.factory.makeOCIRecipeBuild()
55 self.build_url = canonical_url(self.build)62 self.build_url = canonical_url(self.build)
5663
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index 99a0719..ca70e38 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -15,8 +15,10 @@ __all__ = [
15 'IOCIRecipeView',15 'IOCIRecipeView',
16 'NoSourceForOCIRecipe',16 'NoSourceForOCIRecipe',
17 'NoSuchOCIRecipe',17 'NoSuchOCIRecipe',
18 'OCI_RECIPE_ALLOW_CREATE',
18 'OCI_RECIPE_WEBHOOKS_FEATURE_FLAG',19 'OCI_RECIPE_WEBHOOKS_FEATURE_FLAG',
19 'OCIRecipeBuildAlreadyPending',20 'OCIRecipeBuildAlreadyPending',
21 'OCIRecipeFeatureDisabled',
20 'OCIRecipeNotOwner',22 'OCIRecipeNotOwner',
21 ]23 ]
2224
@@ -58,6 +60,16 @@ from lp.services.webhooks.interfaces import IWebhookTarget
5860
5961
60OCI_RECIPE_WEBHOOKS_FEATURE_FLAG = "oci.recipe.webhooks.enabled"62OCI_RECIPE_WEBHOOKS_FEATURE_FLAG = "oci.recipe.webhooks.enabled"
63OCI_RECIPE_ALLOW_CREATE = 'oci.recipe.create.enabled'
64
65
66@error_status(http_client.UNAUTHORIZED)
67class OCIRecipeFeatureDisabled(Unauthorized):
68 """Only certain users can create new LiveFS-related objects."""
69
70 def __init__(self):
71 super(OCIRecipeFeatureDisabled, self).__init__(
72 "You do not have permission to create new OCI recipe.")
6173
6274
63@error_status(http_client.UNAUTHORIZED)75@error_status(http_client.UNAUTHORIZED)
@@ -255,7 +267,7 @@ class IOCIRecipeSet(Interface):
255267
256 def new(name, registrant, owner, oci_project, git_ref, build_file,268 def new(name, registrant, owner, oci_project, git_ref, build_file,
257 description=None, official=False, require_virtualized=True,269 description=None, official=False, require_virtualized=True,
258 date_created=DEFAULT):270 build_daily=False, date_created=DEFAULT):
259 """Create an IOCIRecipe."""271 """Create an IOCIRecipe."""
260272
261 def exists(owner, oci_project, name):273 def exists(owner, oci_project, name):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 68f5e92..21a936b 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -42,7 +42,9 @@ from lp.oci.interfaces.ocirecipe import (
42 IOCIRecipeSet,42 IOCIRecipeSet,
43 NoSourceForOCIRecipe,43 NoSourceForOCIRecipe,
44 NoSuchOCIRecipe,44 NoSuchOCIRecipe,
45 OCI_RECIPE_ALLOW_CREATE,
45 OCIRecipeBuildAlreadyPending,46 OCIRecipeBuildAlreadyPending,
47 OCIRecipeFeatureDisabled,
46 OCIRecipeNotOwner,48 OCIRecipeNotOwner,
47 )49 )
48from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet50from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
@@ -62,6 +64,7 @@ from lp.services.database.stormexpr import (
62 Greatest,64 Greatest,
63 NullsLast,65 NullsLast,
64 )66 )
67from lp.services.features import getFeatureFlag
65from lp.services.webhooks.interfaces import IWebhookSet68from lp.services.webhooks.interfaces import IWebhookSet
66from lp.services.webhooks.model import WebhookTargetMixin69from lp.services.webhooks.model import WebhookTargetMixin
6770
@@ -112,7 +115,9 @@ class OCIRecipe(Storm, WebhookTargetMixin):
112115
113 def __init__(self, name, registrant, owner, oci_project, git_ref,116 def __init__(self, name, registrant, owner, oci_project, git_ref,
114 description=None, official=False, require_virtualized=True,117 description=None, official=False, require_virtualized=True,
115 build_file=None, date_created=DEFAULT):118 build_file=None, build_daily=False, date_created=DEFAULT):
119 if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
120 raise OCIRecipeFeatureDisabled()
116 super(OCIRecipe, self).__init__()121 super(OCIRecipe, self).__init__()
117 self.name = name122 self.name = name
118 self.registrant = registrant123 self.registrant = registrant
@@ -122,6 +127,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
122 self.build_file = build_file127 self.build_file = build_file
123 self.official = official128 self.official = official
124 self.require_virtualized = require_virtualized129 self.require_virtualized = require_virtualized
130 self.build_daily = build_daily
125 self.date_created = date_created131 self.date_created = date_created
126 self.date_last_modified = date_created132 self.date_last_modified = date_created
127 self.git_ref = git_ref133 self.git_ref = git_ref
@@ -276,7 +282,7 @@ class OCIRecipeSet:
276282
277 def new(self, name, registrant, owner, oci_project, git_ref, build_file,283 def new(self, name, registrant, owner, oci_project, git_ref, build_file,
278 description=None, official=False, require_virtualized=True,284 description=None, official=False, require_virtualized=True,
279 date_created=DEFAULT):285 build_daily=False, date_created=DEFAULT):
280 """See `IOCIRecipeSet`."""286 """See `IOCIRecipeSet`."""
281 if not registrant.inTeam(owner):287 if not registrant.inTeam(owner):
282 if owner.is_team:288 if owner.is_team:
@@ -297,7 +303,8 @@ class OCIRecipeSet:
297 store = IMasterStore(OCIRecipe)303 store = IMasterStore(OCIRecipe)
298 oci_recipe = OCIRecipe(304 oci_recipe = OCIRecipe(
299 name, registrant, owner, oci_project, git_ref, description,305 name, registrant, owner, oci_project, git_ref, description,
300 official, require_virtualized, build_file, date_created)306 official, require_virtualized, build_file, build_daily,
307 date_created)
301 store.add(oci_recipe)308 store.add(oci_recipe)
302309
303 return oci_recipe310 return oci_recipe
diff --git a/lib/lp/oci/tests/helpers.py b/lib/lp/oci/tests/helpers.py
index dc0e82f..e199241 100644
--- a/lib/lp/oci/tests/helpers.py
+++ b/lib/lp/oci/tests/helpers.py
@@ -12,6 +12,9 @@ import base64
1212
13from nacl.public import PrivateKey13from nacl.public import PrivateKey
1414
15from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
16from lp.services.features.testing import FeatureFixture
17
1518
16class OCIConfigHelperMixin:19class OCIConfigHelperMixin:
1720
@@ -25,3 +28,5 @@ class OCIConfigHelperMixin:
25 "oci",28 "oci",
26 registry_secrets_private_key=base64.b64encode(29 registry_secrets_private_key=base64.b64encode(
27 bytes(self.private_key)).decode("UTF-8"))30 bytes(self.private_key)).decode("UTF-8"))
31 # Default feature flags for our tests
32 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index 3c60e7e..e15dd8e 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -9,8 +9,8 @@ import base64
9import json9import json
1010
11from fixtures import FakeLogger11from fixtures import FakeLogger
12from six import string_types
13from nacl.public import PrivateKey12from nacl.public import PrivateKey
13from six import string_types
14from storm.exceptions import LostObjectError14from storm.exceptions import LostObjectError
15from testtools.matchers import (15from testtools.matchers import (
16 ContainsDict,16 ContainsDict,
@@ -29,6 +29,7 @@ from lp.oci.interfaces.ocirecipe import (
29 IOCIRecipeSet,29 IOCIRecipeSet,
30 NoSourceForOCIRecipe,30 NoSourceForOCIRecipe,
31 NoSuchOCIRecipe,31 NoSuchOCIRecipe,
32 OCI_RECIPE_ALLOW_CREATE,
32 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,33 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
33 OCIRecipeBuildAlreadyPending,34 OCIRecipeBuildAlreadyPending,
34 OCIRecipeNotOwner,35 OCIRecipeNotOwner,
@@ -60,6 +61,10 @@ class TestOCIRecipe(TestCaseWithFactory):
6061
61 layer = DatabaseFunctionalLayer62 layer = DatabaseFunctionalLayer
6263
64 def setUp(self):
65 super(TestOCIRecipe, self).setUp()
66 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
67
63 def test_implements_interface(self):68 def test_implements_interface(self):
64 target = self.factory.makeOCIRecipe()69 target = self.factory.makeOCIRecipe()
65 with admin_logged_in():70 with admin_logged_in():
@@ -106,7 +111,8 @@ class TestOCIRecipe(TestCaseWithFactory):
106 def test_requestBuild_triggers_webhooks(self):111 def test_requestBuild_triggers_webhooks(self):
107 # Requesting a build triggers webhooks.112 # Requesting a build triggers webhooks.
108 logger = self.useFixture(FakeLogger())113 logger = self.useFixture(FakeLogger())
109 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):114 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
115 OCI_RECIPE_ALLOW_CREATE: 'on'}):
110 recipe = self.factory.makeOCIRecipe()116 recipe = self.factory.makeOCIRecipe()
111 oci_arch = self.factory.makeOCIRecipeArch(recipe=recipe)117 oci_arch = self.factory.makeOCIRecipeArch(recipe=recipe)
112 hook = self.factory.makeWebhook(118 hook = self.factory.makeWebhook(
@@ -153,7 +159,8 @@ class TestOCIRecipe(TestCaseWithFactory):
153159
154 def test_related_webhooks_deleted(self):160 def test_related_webhooks_deleted(self):
155 owner = self.factory.makePerson()161 owner = self.factory.makePerson()
156 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):162 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
163 OCI_RECIPE_ALLOW_CREATE: 'on'}):
157 recipe = self.factory.makeOCIRecipe(registrant=owner, owner=owner)164 recipe = self.factory.makeOCIRecipe(registrant=owner, owner=owner)
158 webhook = self.factory.makeWebhook(target=recipe)165 webhook = self.factory.makeWebhook(target=recipe)
159 with person_logged_in(recipe.owner):166 with person_logged_in(recipe.owner):
@@ -214,6 +221,10 @@ class TestOCIRecipeSet(TestCaseWithFactory):
214221
215 layer = DatabaseFunctionalLayer222 layer = DatabaseFunctionalLayer
216223
224 def setUp(self):
225 super(TestOCIRecipeSet, self).setUp()
226 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
227
217 def test_implements_interface(self):228 def test_implements_interface(self):
218 target_set = getUtility(IOCIRecipeSet)229 target_set = getUtility(IOCIRecipeSet)
219 with admin_logged_in():230 with admin_logged_in():
@@ -382,10 +393,12 @@ class TestOCIRecipeWebservice(TestCaseWithFactory):
382393
383 def setUp(self):394 def setUp(self):
384 super(TestOCIRecipeWebservice, self).setUp()395 super(TestOCIRecipeWebservice, self).setUp()
385 self.person = self.factory.makePerson(displayname="Test Person")396 self.person = self.factory.makePerson(
397 displayname="Test Person")
386 self.webservice = webservice_for_person(398 self.webservice = webservice_for_person(
387 self.person, permission=OAuthPermission.WRITE_PUBLIC,399 self.person, permission=OAuthPermission.WRITE_PUBLIC,
388 default_api_version="devel")400 default_api_version="devel")
401 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
389402
390 def getAbsoluteURL(self, target):403 def getAbsoluteURL(self, target):
391 """Get the webservice absolute URL of the given object or relative404 """Get the webservice absolute URL of the given object or relative
@@ -401,38 +414,39 @@ class TestOCIRecipeWebservice(TestCaseWithFactory):
401414
402 def test_api_get_oci_recipe(self):415 def test_api_get_oci_recipe(self):
403 with person_logged_in(self.person):416 with person_logged_in(self.person):
404 project = removeSecurityProxy(self.factory.makeOCIProject(417 oci_project = self.factory.makeOCIProject(
405 registrant=self.person))418 registrant=self.person)
406 recipe = removeSecurityProxy(self.factory.makeOCIRecipe(419 recipe = self.factory.makeOCIRecipe(
407 oci_project=project))420 oci_project=oci_project)
408 url = api_url(recipe)421 url = api_url(recipe)
409422
410 ws_recipe = self.load_from_api(url)423 ws_recipe = self.load_from_api(url)
411424
412 recipe_abs_url = self.getAbsoluteURL(recipe)425 with person_logged_in(self.person):
413 self.assertThat(ws_recipe, ContainsDict(dict(426 recipe_abs_url = self.getAbsoluteURL(recipe)
414 date_created=Equals(recipe.date_created.isoformat()),427 self.assertThat(ws_recipe, ContainsDict(dict(
415 date_last_modified=Equals(recipe.date_last_modified.isoformat()),428 date_created=Equals(recipe.date_created.isoformat()),
416 registrant_link=Equals(self.getAbsoluteURL(recipe.registrant)),429 date_last_modified=Equals(recipe.date_last_modified.isoformat()),
417 webhooks_collection_link=Equals(recipe_abs_url + "/webhooks"),430 registrant_link=Equals(self.getAbsoluteURL(recipe.registrant)),
418 name=Equals(recipe.name),431 webhooks_collection_link=Equals(recipe_abs_url + "/webhooks"),
419 owner_link=Equals(self.getAbsoluteURL(recipe.owner)),432 name=Equals(recipe.name),
420 oci_project_link=Equals(self.getAbsoluteURL(project)),433 owner_link=Equals(self.getAbsoluteURL(recipe.owner)),
421 git_ref_link=Equals(self.getAbsoluteURL(recipe.git_ref)),434 oci_project_link=Equals(self.getAbsoluteURL(oci_project)),
422 description=Equals(recipe.description),435 git_ref_link=Equals(self.getAbsoluteURL(recipe.git_ref)),
423 build_file=Equals(recipe.build_file),436 description=Equals(recipe.description),
424 build_daily=Equals(recipe.build_daily)437 build_file=Equals(recipe.build_file),
425 )))438 build_daily=Equals(recipe.build_daily)
439 )))
426440
427 def test_api_patch_oci_recipe(self):441 def test_api_patch_oci_recipe(self):
428 with person_logged_in(self.person):442 with person_logged_in(self.person):
429 distro = self.factory.makeDistribution(owner=self.person)443 distro = self.factory.makeDistribution(owner=self.person)
430 project = removeSecurityProxy(self.factory.makeOCIProject(444 oci_project = self.factory.makeOCIProject(
431 pillar=distro, registrant=self.person))445 pillar=distro, registrant=self.person)
432 # Only the owner should be able to edit.446 # Only the owner should be able to edit.
433 recipe = removeSecurityProxy(self.factory.makeOCIRecipe(447 recipe = self.factory.makeOCIRecipe(
434 oci_project=project, owner=self.person,448 oci_project=oci_project, owner=self.person,
435 registrant=self.person))449 registrant=self.person)
436 url = api_url(recipe)450 url = api_url(recipe)
437451
438 new_description = 'Some other description'452 new_description = 'Some other description'
@@ -450,13 +464,13 @@ class TestOCIRecipeWebservice(TestCaseWithFactory):
450 other_person = self.factory.makePerson()464 other_person = self.factory.makePerson()
451 with person_logged_in(other_person):465 with person_logged_in(other_person):
452 distro = self.factory.makeDistribution(owner=other_person)466 distro = self.factory.makeDistribution(owner=other_person)
453 project = removeSecurityProxy(self.factory.makeOCIProject(467 oci_project = self.factory.makeOCIProject(
454 pillar=distro, registrant=other_person))468 pillar=distro, registrant=other_person)
455 # Only the owner should be able to edit.469 # Only the owner should be able to edit.
456 recipe = removeSecurityProxy(self.factory.makeOCIRecipe(470 recipe = self.factory.makeOCIRecipe(
457 oci_project=project, owner=other_person,471 oci_project=oci_project, owner=other_person,
458 registrant=other_person,472 registrant=other_person,
459 description="old description"))473 description="old description")
460 url = api_url(recipe)474 url = api_url(recipe)
461475
462 new_description = 'Some other description'476 new_description = 'Some other description'
@@ -467,3 +481,92 @@ class TestOCIRecipeWebservice(TestCaseWithFactory):
467481
468 ws_project = self.load_from_api(url)482 ws_project = self.load_from_api(url)
469 self.assertEqual("old description", ws_project['description'])483 self.assertEqual("old description", ws_project['description'])
484
485 def test_api_create_oci_recipe(self):
486 with person_logged_in(self.person):
487 distro = self.factory.makeDistribution(
488 owner=self.person)
489 oci_project = self.factory.makeOCIProject(
490 pillar=distro, registrant=self.person)
491 git_ref = self.factory.makeGitRefs()[0]
492
493 oci_project_url = api_url(oci_project)
494 git_ref_url = api_url(git_ref)
495 person_url = api_url(self.person)
496
497 obj = {
498 "name": "my-recipe",
499 "owner": person_url,
500 "git_ref": git_ref_url,
501 "build_file": "./Dockerfile",
502 "description": "My recipe"}
503
504 resp = self.webservice.named_post(oci_project_url, "newRecipe", **obj)
505 self.assertEqual(201, resp.status, resp.body)
506
507 new_obj_url = resp.getHeader("Location")
508 ws_recipe = self.load_from_api(new_obj_url)
509
510 with person_logged_in(self.person):
511 self.assertThat(ws_recipe, ContainsDict(dict(
512 name=Equals(obj["name"]),
513 oci_project_link=Equals(self.getAbsoluteURL(oci_project)),
514 git_ref_link=Equals(self.getAbsoluteURL(git_ref)),
515 build_file=Equals(obj["build_file"]),
516 description=Equals(obj["description"]),
517 owner_link=Equals(self.getAbsoluteURL(self.person)),
518 registrant_link=Equals(self.getAbsoluteURL(self.person)),
519 )))
520
521 def test_api_create_oci_recipe_non_legitimate_user(self):
522 """Ensure that a non-legitimate user cannot create recipe using API"""
523 self.pushConfig(
524 'launchpad', min_legitimate_karma=9999,
525 min_legitimate_account_age=9999)
526
527 with person_logged_in(self.person):
528 distro = self.factory.makeDistribution(
529 owner=self.person)
530 oci_project = self.factory.makeOCIProject(
531 pillar=distro, registrant=self.person)
532 git_ref = self.factory.makeGitRefs()[0]
533
534 oci_project_url = api_url(oci_project)
535 git_ref_url = api_url(git_ref)
536 person_url = api_url(self.person)
537
538 obj = {
539 "name": "My recipe",
540 "owner": person_url,
541 "git_ref": git_ref_url,
542 "build_file": "./Dockerfile",
543 "description": "My recipe"}
544
545 resp = self.webservice.named_post(oci_project_url, "newRecipe", **obj)
546 self.assertEqual(401, resp.status, resp.body)
547
548 def test_api_create_oci_recipe_is_disabled_by_feature_flag(self):
549 """Ensure that OCI newRecipe API method returns HTTP 401 when the
550 feature flag is not set."""
551 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: ''}))
552
553 with person_logged_in(self.person):
554 distro = self.factory.makeDistribution(
555 owner=self.person)
556 oci_project = self.factory.makeOCIProject(
557 pillar=distro, registrant=self.person)
558 git_ref = self.factory.makeGitRefs()[0]
559
560 oci_project_url = api_url(oci_project)
561 git_ref_url = api_url(git_ref)
562 person_url = api_url(self.person)
563
564 obj = {
565 "name": "My recipe",
566 "owner": person_url,
567 "git_ref": git_ref_url,
568 "build_file": "./Dockerfile",
569 "description": "My recipe"}
570
571 resp = self.webservice.named_post(oci_project_url, "newRecipe", **obj)
572 self.assertEqual(401, resp.status, resp.body)
diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
index eec8850..e60a362 100644
--- a/lib/lp/oci/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
@@ -23,7 +23,10 @@ from lp.buildmaster.enums import BuildStatus
23from lp.buildmaster.interfaces.buildqueue import IBuildQueue23from lp.buildmaster.interfaces.buildqueue import IBuildQueue
24from lp.buildmaster.interfaces.packagebuild import IPackageBuild24from lp.buildmaster.interfaces.packagebuild import IPackageBuild
25from lp.buildmaster.interfaces.processor import IProcessorSet25from lp.buildmaster.interfaces.processor import IProcessorSet
26from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG26from lp.oci.interfaces.ocirecipe import (
27 OCI_RECIPE_ALLOW_CREATE,
28 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
29 )
27from lp.oci.interfaces.ocirecipebuild import (30from lp.oci.interfaces.ocirecipebuild import (
28 IOCIRecipeBuild,31 IOCIRecipeBuild,
29 IOCIRecipeBuildSet,32 IOCIRecipeBuildSet,
@@ -54,6 +57,7 @@ class TestOCIRecipeBuild(TestCaseWithFactory):
5457
55 def setUp(self):58 def setUp(self):
56 super(TestOCIRecipeBuild, self).setUp()59 super(TestOCIRecipeBuild, self).setUp()
60 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
57 self.build = self.factory.makeOCIRecipeBuild()61 self.build = self.factory.makeOCIRecipeBuild()
5862
59 def test_implements_interface(self):63 def test_implements_interface(self):
@@ -212,6 +216,10 @@ class TestOCIRecipeBuildSet(TestCaseWithFactory):
212216
213 layer = DatabaseFunctionalLayer217 layer = DatabaseFunctionalLayer
214218
219 def setUp(self):
220 super(TestOCIRecipeBuildSet, self).setUp()
221 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
222
215 def test_implements_interface(self):223 def test_implements_interface(self):
216 target = OCIRecipeBuildSet()224 target = OCIRecipeBuildSet()
217 with admin_logged_in():225 with admin_logged_in():
@@ -241,7 +249,8 @@ class TestOCIRecipeBuildSet(TestCaseWithFactory):
241 distribution=distribution, status=SeriesStatus.CURRENT)249 distribution=distribution, status=SeriesStatus.CURRENT)
242 processor = getUtility(IProcessorSet).getByName("386")250 processor = getUtility(IProcessorSet).getByName("386")
243 self.useFixture(FeatureFixture({251 self.useFixture(FeatureFixture({
244 "oci.build_series.%s" % distribution.name: distroseries.name}))252 "oci.build_series.%s" % distribution.name: distroseries.name,
253 OCI_RECIPE_ALLOW_CREATE: 'on'}))
245 distro_arch_series = self.factory.makeDistroArchSeries(254 distro_arch_series = self.factory.makeDistroArchSeries(
246 distroseries=distroseries, architecturetag="i386",255 distroseries=distroseries, architecturetag="i386",
247 processor=processor)256 processor=processor)
diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
index 58b6adc..1d9328b 100644
--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for `OCIRecipeBuildBehaviour`."""4"""Tests for `OCIRecipeBuildBehaviour`."""
@@ -63,6 +63,7 @@ from lp.buildmaster.tests.snapbuildproxy import (
63from lp.buildmaster.tests.test_buildfarmjobbehaviour import (63from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
64 TestGetUploadMethodsMixin,64 TestGetUploadMethodsMixin,
65 )65 )
66from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
66from lp.oci.model.ocirecipebuildbehaviour import OCIRecipeBuildBehaviour67from lp.oci.model.ocirecipebuildbehaviour import OCIRecipeBuildBehaviour
67from lp.registry.interfaces.series import SeriesStatus68from lp.registry.interfaces.series import SeriesStatus
68from lp.services.config import config69from lp.services.config import config
@@ -123,6 +124,10 @@ class TestOCIBuildBehaviour(TestCaseWithFactory):
123124
124 layer = LaunchpadZopelessLayer125 layer = LaunchpadZopelessLayer
125126
127 def setUp(self):
128 super(TestOCIBuildBehaviour, self).setUp()
129 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
130
126 def test_provides_interface(self):131 def test_provides_interface(self):
127 # OCIRecipeBuildBehaviour provides IBuildFarmJobBehaviour.132 # OCIRecipeBuildBehaviour provides IBuildFarmJobBehaviour.
128 job = OCIRecipeBuildBehaviour(self.factory.makeOCIRecipeBuild())133 job = OCIRecipeBuildBehaviour(self.factory.makeOCIRecipeBuild())
@@ -159,6 +164,7 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory):
159 self.now = time.time()164 self.now = time.time()
160 self.useFixture(fixtures.MockPatch(165 self.useFixture(fixtures.MockPatch(
161 "time.time", return_value=self.now))166 "time.time", return_value=self.now))
167 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
162168
163 @defer.inlineCallbacks169 @defer.inlineCallbacks
164 def test_composeBuildRequest(self):170 def test_composeBuildRequest(self):
@@ -350,7 +356,8 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory):
350 distribution=distribution, status=SeriesStatus.CURRENT)356 distribution=distribution, status=SeriesStatus.CURRENT)
351 processor = getUtility(IProcessorSet).getByName("386")357 processor = getUtility(IProcessorSet).getByName("386")
352 self.useFixture(FeatureFixture({358 self.useFixture(FeatureFixture({
353 "oci.build_series.%s" % distribution.name: distroseries.name}))359 "oci.build_series.%s" % distribution.name: distroseries.name,
360 OCI_RECIPE_ALLOW_CREATE: 'on'}))
354 distro_arch_series = self.factory.makeDistroArchSeries(361 distro_arch_series = self.factory.makeDistroArchSeries(
355 distroseries=distroseries, architecturetag="i386",362 distroseries=distroseries, architecturetag="i386",
356 processor=processor)363 processor=processor)
@@ -401,6 +408,7 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
401 def setUp(self):408 def setUp(self):
402 super(TestHandleStatusForOCIRecipeBuild, self).setUp()409 super(TestHandleStatusForOCIRecipeBuild, self).setUp()
403 self.useFixture(fixtures.FakeLogger())410 self.useFixture(fixtures.FakeLogger())
411 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
404 self.build = self.makeBuild()412 self.build = self.makeBuild()
405 # For the moment, we require a builder for the build so that413 # For the moment, we require a builder for the build so that
406 # handleStatus_OK can get a reference to the slave.414 # handleStatus_OK can get a reference to the slave.
@@ -627,3 +635,6 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
627class TestGetUploadMethodsForOCIRecipeBuild(635class TestGetUploadMethodsForOCIRecipeBuild(
628 MakeOCIBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):636 MakeOCIBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
629 """IPackageBuild.getUpload-related methods work with OCI recipe builds."""637 """IPackageBuild.getUpload-related methods work with OCI recipe builds."""
638 def setUp(self):
639 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
640 super(TestGetUploadMethodsForOCIRecipeBuild, self).setUp()
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index d5875c5..f3bcd9f 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -745,6 +745,9 @@
745 permission="launchpad.Edit"745 permission="launchpad.Edit"
746 interface="lp.registry.interfaces.ociproject.IOCIProjectEdit"746 interface="lp.registry.interfaces.ociproject.IOCIProjectEdit"
747 set_schema="lp.registry.interfaces.ociproject.IOCIProjectEditableAttributes" />747 set_schema="lp.registry.interfaces.ociproject.IOCIProjectEditableAttributes" />
748 <require
749 permission="launchpad.AnyLegitimatePerson"
750 interface="lp.registry.interfaces.ociproject.IOCIProjectLegitimate"/>
748 </class>751 </class>
749 <subscriber752 <subscriber
750 for="lp.registry.interfaces.ociproject.IOCIProject zope.lifecycleevent.interfaces.IObjectModifiedEvent"753 for="lp.registry.interfaces.ociproject.IOCIProject zope.lifecycleevent.interfaces.IObjectModifiedEvent"
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index b39c71f..8a3e734 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -13,16 +13,23 @@ __all__ = [
13 ]13 ]
1414
15from lazr.restful.declarations import (15from lazr.restful.declarations import (
16 call_with,
16 export_as_webservice_entry,17 export_as_webservice_entry,
18 export_factory_operation,
17 exported,19 exported,
20 operation_for_version,
21 operation_parameters,
22 REQUEST_USER,
18 )23 )
19from lazr.restful.fields import (24from lazr.restful.fields import (
20 CollectionField,25 CollectionField,
21 Reference,26 Reference,
22 ReferenceChoice,27 ReferenceChoice,
23 )28 )
29from lp.app.validators.path import path_does_not_escape
24from zope.interface import Interface30from zope.interface import Interface
25from zope.schema import (31from zope.schema import (
32 Bool,
26 Datetime,33 Datetime,
27 Int,34 Int,
28 Text,35 Text,
@@ -32,12 +39,16 @@ from zope.schema import (
32from lp import _39from lp import _
33from lp.app.validators.name import name_validator40from lp.app.validators.name import name_validator
34from lp.bugs.interfaces.bugtarget import IBugTarget41from lp.bugs.interfaces.bugtarget import IBugTarget
42from lp.code.interfaces.gitref import IGitRef
35from lp.code.interfaces.hasgitrepositories import IHasGitRepositories43from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
36from lp.registry.interfaces.distribution import IDistribution44from lp.registry.interfaces.distribution import IDistribution
37from lp.registry.interfaces.ociprojectname import IOCIProjectName45from lp.registry.interfaces.ociprojectname import IOCIProjectName
38from lp.registry.interfaces.series import SeriesStatus46from lp.registry.interfaces.series import SeriesStatus
39from lp.services.database.constants import DEFAULT47from lp.services.database.constants import DEFAULT
40from lp.services.fields import PublicPersonChoice48from lp.services.fields import (
49 PersonChoice,
50 PublicPersonChoice,
51 )
4152
4253
43OCI_PROJECT_ALLOW_CREATE = 'oci.project.create.enabled'54OCI_PROJECT_ALLOW_CREATE = 'oci.project.create.enabled'
@@ -105,8 +116,44 @@ class IOCIProjectEdit(Interface):
105 """Creates a new `IOCIProjectSeries`."""116 """Creates a new `IOCIProjectSeries`."""
106117
107118
119class IOCIProjectLegitimate(Interface):
120 """IOCIProject methods that require launchpad.AnyLegitimatePerson
121 permission.
122 """
123 @call_with(registrant=REQUEST_USER)
124 @operation_parameters(
125 name=TextLine(
126 title=_("OCI Recipe name."),
127 description=_("The name of the new OCI Recipe."),
128 required=True),
129 owner=PersonChoice(
130 title=_("Person or team that owns the new OCI Recipe."),
131 vocabulary="AllUserTeamsParticipationPlusSelf",
132 required=True),
133 git_ref=Reference(IGitRef, title=_("Git branch."), required=True),
134 build_file=TextLine(
135 title=_("Build file path."),
136 description=_(
137 "The relative path to the file within this recipe's "
138 "branch that defines how to build the recipe."),
139 constraint=path_does_not_escape,
140 required=True),
141 description=Text(
142 title=_("Description for this recipe."),
143 description=_("A short description of this recipe."),
144 required=False),
145 build_daily=Bool(
146 title=_("Should this recipe be built daily?."), required=False))
147 @export_factory_operation(Interface, [])
148 @operation_for_version("devel")
149 def newRecipe(name, registrant, owner, git_ref, build_file,
150 description=None, build_daily=False,
151 require_virtualized=True):
152 """Create an IOCIRecipe for this project."""
153
154
108class IOCIProject(IOCIProjectView, IOCIProjectEdit,155class IOCIProject(IOCIProjectView, IOCIProjectEdit,
109 IOCIProjectEditableAttributes):156 IOCIProjectEditableAttributes, IOCIProjectLegitimate):
110 """A project containing Open Container Initiative recipes."""157 """A project containing Open Container Initiative recipes."""
111158
112 export_as_webservice_entry(159 export_as_webservice_entry(
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index aa3a322..2967007 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -25,6 +25,7 @@ from zope.interface import implementer
25from zope.security.proxy import removeSecurityProxy25from zope.security.proxy import removeSecurityProxy
2626
27from lp.bugs.model.bugtarget import BugTargetBase27from lp.bugs.model.bugtarget import BugTargetBase
28from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
28from lp.registry.interfaces.distribution import IDistribution29from lp.registry.interfaces.distribution import IDistribution
29from lp.registry.interfaces.ociproject import (30from lp.registry.interfaces.ociproject import (
30 IOCIProject,31 IOCIProject,
@@ -107,6 +108,21 @@ class OCIProject(BugTargetBase, StormBase):
107 bugtargetname = display_name108 bugtargetname = display_name
108 bugtargetdisplayname = display_name109 bugtargetdisplayname = display_name
109110
111 def newRecipe(self, name, registrant, owner, git_ref,
112 build_file, description=None, build_daily=False,
113 require_virtualized=True):
114 return getUtility(IOCIRecipeSet).new(
115 name=name,
116 registrant=registrant,
117 owner=owner,
118 oci_project=self,
119 git_ref=git_ref,
120 build_file=build_file,
121 description=description,
122 require_virtualized=require_virtualized,
123 build_daily=build_daily,
124 )
125
110 def newSeries(self, name, summary, registrant,126 def newSeries(self, name, summary, registrant,
111 status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT):127 status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT):
112 """See `IOCIProject`."""128 """See `IOCIProject`."""
diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py
index 890b421..02fefcc 100644
--- a/lib/lp/registry/tests/test_personmerge.py
+++ b/lib/lp/registry/tests/test_personmerge.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for merge_people."""4"""Tests for merge_people."""
@@ -18,7 +18,10 @@ from zope.security.proxy import removeSecurityProxy
18from lp.app.enums import InformationType18from lp.app.enums import InformationType
19from lp.app.interfaces.launchpad import ILaunchpadCelebrities19from lp.app.interfaces.launchpad import ILaunchpadCelebrities
20from lp.code.interfaces.gitrepository import IGitRepositorySet20from lp.code.interfaces.gitrepository import IGitRepositorySet
21from lp.oci.interfaces.ocirecipe import IOCIRecipeSet21from lp.oci.interfaces.ocirecipe import (
22 IOCIRecipeSet,
23 OCI_RECIPE_ALLOW_CREATE,
24 )
22from lp.registry.interfaces.accesspolicy import (25from lp.registry.interfaces.accesspolicy import (
23 IAccessArtifactGrantSource,26 IAccessArtifactGrantSource,
24 IAccessPolicyGrantSource,27 IAccessPolicyGrantSource,
@@ -664,6 +667,7 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
664 def test_merge_moves_oci_recipes(self):667 def test_merge_moves_oci_recipes(self):
665 # When person/teams are merged, oci recipes owned by the from668 # When person/teams are merged, oci recipes owned by the from
666 # person are moved.669 # person are moved.
670 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
667 duplicate = self.factory.makePerson()671 duplicate = self.factory.makePerson()
668 mergee = self.factory.makePerson()672 mergee = self.factory.makePerson()
669 self.factory.makeOCIRecipe(registrant=duplicate, owner=duplicate)673 self.factory.makeOCIRecipe(registrant=duplicate, owner=duplicate)
@@ -676,6 +680,7 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
676 def test_merge_with_duplicated_oci_recipes(self):680 def test_merge_with_duplicated_oci_recipes(self):
677 # If both the from and to people have oci recipes with the same681 # If both the from and to people have oci recipes with the same
678 # name, merging renames the duplicate from the from person's side.682 # name, merging renames the duplicate from the from person's side.
683 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
679 duplicate = self.factory.makePerson()684 duplicate = self.factory.makePerson()
680 mergee = self.factory.makePerson()685 mergee = self.factory.makePerson()
681 [ref] = self.factory.makeGitRefs()686 [ref] = self.factory.makeGitRefs()
diff --git a/lib/lp/services/webhooks/tests/test_job.py b/lib/lp/services/webhooks/tests/test_job.py
index caf81ce..fde24da 100644
--- a/lib/lp/services/webhooks/tests/test_job.py
+++ b/lib/lp/services/webhooks/tests/test_job.py
@@ -37,7 +37,10 @@ from zope.component import getUtility
37from zope.security.proxy import removeSecurityProxy37from zope.security.proxy import removeSecurityProxy
3838
39from lp.app import versioninfo39from lp.app import versioninfo
40from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG40from lp.oci.interfaces.ocirecipe import (
41 OCI_RECIPE_ALLOW_CREATE,
42 OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
43 )
41from lp.services.database.interfaces import IStore44from lp.services.database.interfaces import IStore
42from lp.services.features.testing import FeatureFixture45from lp.services.features.testing import FeatureFixture
43from lp.services.job.interfaces.job import JobStatus46from lp.services.job.interfaces.job import JobStatus
@@ -358,7 +361,8 @@ class TestWebhookDeliveryJob(TestCaseWithFactory):
358 def test_oci_recipe__repr__(self):361 def test_oci_recipe__repr__(self):
359 # `WebhookDeliveryJob` objects for OCI recipes have an informative362 # `WebhookDeliveryJob` objects for OCI recipes have an informative
360 # __repr__.363 # __repr__.
361 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):364 with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
365 OCI_RECIPE_ALLOW_CREATE: 'on'}):
362 recipe = self.factory.makeOCIRecipe()366 recipe = self.factory.makeOCIRecipe()
363 hook = self.factory.makeWebhook(target=recipe)367 hook = self.factory.makeWebhook(target=recipe)
364 job = WebhookDeliveryJob.create(hook, 'test', payload={'foo': 'bar'})368 job = WebhookDeliveryJob.create(hook, 'test', payload={'foo': 'bar'})