launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24143
[Merge] ~twom/launchpad:concrete-oci-projects into launchpad:master
Tom Wardill has proposed merging ~twom/launchpad:concrete-oci-projects into launchpad:master with ~twom/launchpad:oci-gitrepository as a prerequisite.
Commit message:
OCIRecipe and skeleton of dependant models
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/376132
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:concrete-oci-projects into launchpad:master.
diff --git a/lib/lp/buildmaster/enums.py b/lib/lp/buildmaster/enums.py
index c6f0aac..1eae263 100644
--- a/lib/lp/buildmaster/enums.py
+++ b/lib/lp/buildmaster/enums.py
@@ -164,6 +164,12 @@ class BuildFarmJobType(DBEnumeratedType):
Build a snap package from a recipe.
""")
+ OCIBUILD = DBItem(7, """
+ OCI image build
+
+ Build an OCI image from a recipe.
+ """)
+
class BuildQueueStatus(DBEnumeratedType):
"""Build queue status.
diff --git a/lib/lp/configure.zcml b/lib/lp/configure.zcml
index 533d714..a3e7c08 100644
--- a/lib/lp/configure.zcml
+++ b/lib/lp/configure.zcml
@@ -30,6 +30,7 @@
<include package="lp.code" />
<include package="lp.coop.answersbugs" />
<include package="lp.hardwaredb" />
+ <include package="lp.oci" />
<include package="lp.snappy" />
<include package="lp.soyuz" />
<include package="lp.translations" />
diff --git a/lib/lp/oci/__init__.py b/lib/lp/oci/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/oci/__init__.py
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
new file mode 100644
index 0000000..a301911
--- /dev/null
+++ b/lib/lp/oci/configure.zcml
@@ -0,0 +1,58 @@
+<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ xmlns:lp="http://namespaces.canonical.com/lp"
+ xmlns:webservice="http://namespaces.canonical.com/webservice"
+ xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
+ i18n_domain="launchpad">
+
+ <!-- ocirecipe -->
+ <class
+ class="lp.oci.model.ocirecipe.OCIRecipe">
+ <require
+ permission="launchpad.View"
+ interface="lp.oci.interfaces.ocirecipe.IOCIRecipeView
+ lp.oci.interfaces.ocirecipe.IOCIRecipeEditableAttributes"/>
+ <require
+ permission="launchpad.Edit"
+ interface="lp.oci.interfaces.ocirecipe.IOCIRecipeEdit"
+ set_schema="lp.oci.interfaces.ocirecipe.IOCIRecipeEditableAttributes" />
+ </class>
+ <securedutility
+ class="lp.oci.model.ocirecipe.OCIRecipeSet"
+ provides="lp.oci.interfaces.ocirecipe.IOCIRecipeSet">
+ <allow
+ interface="lp.oci.interfaces.ocirecipe.IOCIRecipeSet"/>
+ </securedutility>
+
+ <!-- OCIRecipeBuild -->
+ <class class="lp.oci.model.ocirecipebuild.OCIRecipeBuild">
+ <require
+ permission="launchpad.View"
+ interface="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildView" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildEdit" />
+ <require
+ permission="launchpad.Admin"
+ interface="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildAdmin" />
+ </class>
+
+ <!-- OCIRecipeBuildSet -->
+ <securedutility
+ class="lp.oci.model.ocirecipebuild.OCIRecipeBuildSet"
+ provides="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildSet">
+ <allow interface="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildSet" />
+ </securedutility>
+ <securedutility
+ class="lp.oci.model.ocirecipebuild.OCIRecipeBuildSet"
+ provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"
+ name="OCIRECIPEBUILD">
+ <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
+ </securedutility>
+
+</configure>
diff --git a/lib/lp/oci/interfaces/__init__.py b/lib/lp/oci/interfaces/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/oci/interfaces/__init__.py
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
new file mode 100644
index 0000000..969f52f
--- /dev/null
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -0,0 +1,103 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces related to recipes for OCI Images."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IOCIRecipe',
+ 'IOCIRecipeEdit',
+ 'IOCIRecipeEditableAttributes',
+ 'IOCIRecipeSet',
+ 'IOCIRecipeView',
+ 'OCIBuildAlreadyPending',
+ 'OCIRecipeNotOwner',
+ ]
+
+import httplib
+
+from lazr.restful.declarations import error_status
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Bool,
+ Datetime,
+ Int,
+ Text,
+ )
+from zope.security.interfaces import Unauthorized
+
+from lp import _
+from lp.registry.interfaces.ociproject import IOCIProject
+from lp.registry.interfaces.role import IHasOwner
+from lp.services.fields import PublicPersonChoice
+
+
+@error_status(httplib.UNAUTHORIZED)
+class OCIRecipeNotOwner(Unauthorized):
+ """The registrant/requester is not the owner or a member of its team."""
+
+
+@error_status(httplib.BAD_REQUEST)
+class OCIBuildAlreadyPending(Exception):
+ """A build was requested when an identical build was already pending."""
+
+ def __init__(self):
+ super(OCIBuildAlreadyPending, self).__init__(
+ "An identical build of this snap package is already pending.")
+
+
+class IOCIRecipeView(Interface):
+ """`IOCIRecipe` attributes that require launchpad.View permission."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True)
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True)
+
+ registrant = PublicPersonChoice(
+ title=_("Registrant"),
+ description=_("The user who registered this recipe."),
+ vocabulary='ValidPersonOrTeam', required=True, readonly=True)
+
+
+class IOCIRecipeEdit(Interface):
+ """`IOCIRecipe` methods that require launchpad.Edit permission."""
+
+ def destroySelf():
+ """Delete this snap package, provided that it has no builds."""
+
+
+class IOCIRecipeEditableAttributes(IHasOwner):
+ """`IOCIRecipe` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ ociproject = Reference(
+ IOCIProject,
+ title=_("The OCI project that this recipe is for."),
+ required=True,
+ readonly=True)
+ ociproject_default = Bool(
+ title=_("OCI Project default"), required=True, default=False)
+
+ description = Text(title=_("A short description of this recipe."))
+
+ require_virtualized = Bool(
+ title=_("Require virtualized"), required=True, default=True)
+
+
+class IOCIRecipe(IOCIRecipeView, IOCIRecipeEdit, IOCIRecipeEditableAttributes):
+ """A recipe for building Open Container Initiative images."""
+
+
+class IOCIRecipeSet(Interface):
+ """A utility to create and access OCI Recipes."""
+
+ def new(registrant, owner, ociproject, ociproject_default,
+ require_virtualized):
+ """Create an IOCIRecipe."""
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
new file mode 100644
index 0000000..e3bf54f
--- /dev/null
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -0,0 +1,56 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for a build record for OCI recipes."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IOCIRecipeBuild',
+ 'IOCIRecipeBuildSet'
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+
+from lp import _
+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+from lp.services.database.constants import DEFAULT
+from lp.services.fields import PublicPersonChoice
+
+
+class IOCIRecipeBuildEdit(Interface):
+ pass
+
+
+class IOCIRecipeBuildView(IPackageBuild):
+
+ requester = PublicPersonChoice(
+ title=_("Requester"),
+ description=_("The person who requested this OCI recipe build."),
+ vocabulary='ValidPersonOrTeam', required=True, readonly=True)
+
+ recipe = Reference(
+ IOCIRecipe,
+ title=_("The OCI recipe to build."),
+ required=True,
+ readonly=True)
+
+
+class IOCIRecipeBuildAdmin(Interface):
+ pass
+
+
+class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit,
+ IOCIRecipeBuildView):
+ """A build record for an OCI recipe."""
+
+
+class IOCIRecipeBuildSet(Interface):
+ """A utility to create and access OCIRecipeBuilds."""
+
+ def new(requester, recipe, channel_name, processor, virtualized,
+ date_created=DEFAULT):
+ """Create an `IOCIRecipeBuild`."""
diff --git a/lib/lp/oci/interfaces/ocirecipechannel.py b/lib/lp/oci/interfaces/ocirecipechannel.py
new file mode 100644
index 0000000..e4871a9
--- /dev/null
+++ b/lib/lp/oci/interfaces/ocirecipechannel.py
@@ -0,0 +1,42 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for defining channel attributes for an OCI Recipe."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IOCIRecipeChannel',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import TextLine
+
+from lp import _
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+
+
+class IOCIRecipeChannel(Interface):
+ """The channels that exist for an OCI recipe."""
+
+ recipe = Reference(
+ IOCIRecipe,
+ title=_("The OCI recipe for which a channel is specified."),
+ required=True,
+ readonly=True)
+
+ name = TextLine(
+ title=_("The name of this channel."),
+ required=True)
+
+ git_path = TextLine(
+ title=_("The branch within this recipes Git "
+ "repository where its build files are maintained."),
+ required=True)
+
+ build_file = TextLine(
+ title=_("The relative path to the file within this recipe's "
+ "branch that defines how to build the recipe."),
+ required=True)
diff --git a/lib/lp/oci/model/__init__.py b/lib/lp/oci/model/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/oci/model/__init__.py
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
new file mode 100644
index 0000000..66755a5
--- /dev/null
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -0,0 +1,150 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A recipe for building Open Container Initiative images."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OCIRecipe',
+ 'OCIRecipeSet',
+ ]
+
+
+from lazr.lifecycle.event import ObjectCreatedEvent
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
+
+from lp.buildmaster.enums import BuildStatus
+from lp.oci.interfaces.ocirecipe import (
+ IOCIRecipe,
+ IOCIRecipeSet,
+ OCIBuildAlreadyPending,
+ OCIRecipeNotOwner,
+ )
+from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.oci.model.ocirecipebuild import OCIRecipeBuild
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+
+
+@implementer(IOCIRecipe)
+class OCIRecipe(Storm):
+
+ __storm_table__ = 'OCIRecipe'
+
+ id = Int(primary=True)
+ date_created = DateTime(
+ name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ date_last_modified = DateTime(
+ name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
+
+ registrant_id = Int(name='registrant', allow_none=False)
+ registrant = Reference(registrant_id, "Person.id")
+
+ owner_id = Int(name='owner', allow_none=False)
+ owner = Reference(owner_id, 'Person.id')
+
+ ociproject_id = Int(name='ociproject', allow_none=False)
+ ociproject = Reference(ociproject_id, "OCIProject.id")
+
+ ociproject_default = Bool(name="ociproject_default", default=False)
+
+ description = Unicode(name="description")
+
+ require_virtualized = Bool(name="require_virtualized", default=True)
+
+ def __init__(self, registrant, owner, ociproject, ociproject_default=False,
+ require_virtualized=True):
+ super(OCIRecipe, self).__init__()
+ self.registrant = registrant
+ self.owner = owner
+ self.ociproject = ociproject
+ self.ociproject_default = ociproject_default
+ self.require_virtualized = require_virtualized
+
+ def destroySelf(self):
+ """See `IOCIRecipe`."""
+ # XXX twom 2019-11-26 This needs to expand as more build artifacts
+ # are added
+ store = IStore(OCIRecipe)
+ store.remove(self)
+
+ def _checkRequestBuild(self, requester):
+ if not requester.inTeam(self.owner):
+ raise OCIRecipeNotOwner(
+ "%s cannot create OCI image builds owned by %s." %
+ (requester.displayname, self.owner.displayname))
+
+ def requestBuild(self, requester, channel, architecture):
+ self._checkRequestBuild(requester)
+
+ pending = IStore(self).find(
+ OCIRecipeBuild,
+ OCIRecipeBuild.recipe == self.id,
+ OCIRecipeBuild.channel_name == channel.name,
+ OCIRecipeBuild.status == BuildStatus.NEEDSBUILD)
+ if pending.any() is not None:
+ raise OCIBuildAlreadyPending
+
+ build = getUtility(IOCIRecipeBuildSet).new(
+ requester, self, channel.name, architecture.processor,
+ self.require_virtualized)
+ build.queueBuild()
+ notify(ObjectCreatedEvent(build, user=requester))
+ return build
+
+
+class OCIRecipeArch(Storm):
+ """Link table back to `OCIRecipe.processors`."""
+
+ __storm_table__ = "OCIRecipeArch"
+ __storm_primary__ = ("recipe_id", "processor_id")
+
+ recipe_id = Int(name="recipe", allow_none=False)
+ recipe = Reference(recipe_id, "OCIRecipe.id")
+
+ processor_id = Int(name="processor", allow_none=False)
+ processor = Reference(processor_id, "Processor.id")
+
+ def __init__(self, recipe, processor):
+ self.recipe = recipe
+ self.processor = processor
+
+
+@implementer(IOCIRecipeSet)
+class OCIRecipeSet:
+
+ def new(self, registrant, owner, ociproject, ociproject_default,
+ require_virtualized):
+ """See `IOCIRecipeSet`."""
+ if not registrant.inTeam(owner):
+ if owner.is_team:
+ raise OCIRecipeNotOwner(
+ "%s is not a member of %s." %
+ (registrant.displayname, owner.displayname))
+ else:
+ raise OCIRecipeNotOwner(
+ "%s cannot create OCI images owned by %s." %
+ (registrant.displayname, owner.displayname))
+
+ store = IMasterStore(OCIRecipe)
+ oci_recipe = OCIRecipe(
+ registrant, owner, ociproject, ociproject_default,
+ require_virtualized)
+ store.add(oci_recipe)
+
+ return oci_recipe
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
new file mode 100644
index 0000000..987ac86
--- /dev/null
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -0,0 +1,120 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A build record for OCI Recipes."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OCIRecipeBuild',
+ 'OCIRecipeBuildSet'
+ ]
+
+
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.component import getUtility
+from zope.interface import implementer
+
+from lp.buildmaster.enums import (
+ BuildFarmJobType,
+ BuildStatus,
+ )
+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
+from lp.buildmaster.model.packagebuild import PackageBuildMixin
+from lp.oci.interfaces.ocirecipebuild import (
+ IOCIRecipeBuild,
+ IOCIRecipeBuildSet,
+ )
+from lp.services.database.constants import DEFAULT
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.interfaces import IMasterStore
+
+
+@implementer(IOCIRecipeBuild)
+class OCIRecipeBuild(PackageBuildMixin, Storm):
+
+ __storm_table__ = 'OCIRecipeBuild'
+
+ job_type = BuildFarmJobType.OCIBUILD
+
+ id = Int(name='id', primary=True)
+
+ build_farm_job_id = Int(name='build_farm_job', allow_none=False)
+ build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id')
+
+ requester_id = Int(name='requester', allow_none=False)
+ requester = Reference(requester_id, 'Person.id')
+
+ recipe_id = Int(name='recipe', allow_none=False)
+ recipe = Reference(recipe_id, 'OCIRecipe.id')
+
+ channel_name = Unicode(name="channel_name", allow_none=False)
+
+ processor_id = Int(name='processor', allow_none=False)
+ processor = Reference(processor_id, 'Processor.id')
+ virtualized = Bool(name='virtualized')
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+ date_started = DateTime(name='date_started', tzinfo=pytz.UTC)
+ date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC)
+ date_first_dispatched = DateTime(
+ name='date_first_dispatched', tzinfo=pytz.UTC)
+
+ builder_id = Int(name='builder')
+ builder = Reference(builder_id, 'Builder.id')
+
+ status = DBEnum(name='status', enum=BuildStatus, allow_none=False)
+
+ log_id = Int(name='log')
+ log = Reference(log_id, 'LibraryFileAlias.id')
+
+ upload_log_id = Int(name='upload_log')
+ upload_log = Reference(upload_log_id, 'LibraryFileAlias.id')
+
+ dependencies = Unicode(name='dependencies')
+
+ failure_count = Int(name='failure_count', allow_none=False)
+
+ def __init__(self, build_farm_job, requester, recipe, channel_name,
+ processor, virtualized, date_created):
+
+ self.build_farm_job = build_farm_job
+ self.requester = requester
+ self.recipe = recipe
+ self.channel_name = channel_name
+ self.processor = processor
+ self.virtualized = virtualized
+ self.date_created = date_created
+ self.status = BuildStatus.NEEDSBUILD
+
+ def queueBuild(self):
+ """See `IPackageBuild`."""
+ # XXX twom 2019-11-28 Currently a no-op skeleton, to be filled in
+ return
+
+
+@implementer(IOCIRecipeBuildSet)
+class OCIRecipeBuildSet:
+ """See `IOCIRecipeBuildSet`."""
+
+ def new(self, requester, recipe, channel_name, processor, virtualized,
+ date_created=DEFAULT):
+ """See `IOCIRecipeBuildSet`."""
+ store = IMasterStore(OCIRecipeBuild)
+ build_farm_job = getUtility(IBuildFarmJobSource).new(
+ OCIRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created)
+ ocirecipebuild = OCIRecipeBuild(
+ build_farm_job, requester, recipe, channel_name, processor,
+ virtualized, date_created)
+ store.add(ocirecipebuild)
+ return ocirecipebuild
diff --git a/lib/lp/oci/model/ocirecipechannel.py b/lib/lp/oci/model/ocirecipechannel.py
new file mode 100644
index 0000000..248279e
--- /dev/null
+++ b/lib/lp/oci/model/ocirecipechannel.py
@@ -0,0 +1,42 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A recipe for building Open Container Initiative images."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'OCIRecipeChannel',
+ ]
+
+
+from storm.locals import (
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.oci.interfaces.ocirecipechannel import IOCIRecipeChannel
+
+
+@implementer(IOCIRecipeChannel)
+class OCIRecipeChannel(Storm):
+
+ __storm_table__ = "OCIRecipeChannel"
+ __storm_primary__ = ("recipe_id", "name")
+
+ recipe_id = Int(name="recipe", allow_none=False)
+ recipe = Reference(recipe_id, "OCIRecipe.id")
+
+ name = Unicode(name="name", allow_none=False)
+ git_path = Unicode(name="git_path", allow_none=False)
+ build_file = Unicode(name="build_file", allow_none=False)
+
+ def __init__(self, recipe, name, git_path, build_file):
+ self.recipe = recipe
+ self.name = name
+ self.git_path = git_path
+ self.build_file = build_file
diff --git a/lib/lp/oci/tests/__init__.py b/lib/lp/oci/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/oci/tests/__init__.py
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
new file mode 100644
index 0000000..a35ccbf
--- /dev/null
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -0,0 +1,79 @@
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.buildmaster.enums import BuildStatus
+from lp.oci.interfaces.ocirecipe import (
+ IOCIRecipe,
+ IOCIRecipeSet,
+ OCIBuildAlreadyPending,
+ OCIRecipeNotOwner,
+ )
+from lp.testing import (
+ admin_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestOCIRecipe(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_interface(self):
+ target = self.factory.makeOCIRecipe()
+ with admin_logged_in():
+ self.assertProvides(target, IOCIRecipe)
+
+ def test_checkRequestBuild(self):
+ ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe())
+ unrelated_person = self.factory.makePerson()
+ self.assertRaises(
+ OCIRecipeNotOwner,
+ ocirecipe._checkRequestBuild,
+ unrelated_person)
+
+ def test_requestBuild(self):
+ ocirecipe = self.factory.makeOCIRecipe()
+ ocirecipechannel = self.factory.makeOCIRecipeChannel(recipe=ocirecipe)
+ oci_arch = self.factory.makeOCIRecipeArch(recipe=ocirecipe)
+ build = ocirecipe.requestBuild(
+ ocirecipe.owner, ocirecipechannel, oci_arch)
+ self.assertEqual(build.status, BuildStatus.NEEDSBUILD)
+
+ def test_requestBuild_already_exists(self):
+ ocirecipe = self.factory.makeOCIRecipe()
+ ocirecipechannel = self.factory.makeOCIRecipeChannel(recipe=ocirecipe)
+ oci_arch = self.factory.makeOCIRecipeArch(recipe=ocirecipe)
+ ocirecipe.requestBuild(
+ ocirecipe.owner, ocirecipechannel, oci_arch)
+
+ self.assertRaises(
+ OCIBuildAlreadyPending,
+ ocirecipe.requestBuild,
+ ocirecipe.owner, ocirecipechannel, oci_arch)
+
+
+class TestOCIRecipeSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_interface(self):
+ target_set = getUtility(IOCIRecipeSet)
+ with admin_logged_in():
+ self.assertProvides(target_set, IOCIRecipeSet)
+
+ def test_new(self):
+ registrant = self.factory.makePerson()
+ owner = self.factory.makeTeam(members=[registrant])
+ ociproject = self.factory.makeOCIProject()
+ target = getUtility(IOCIRecipeSet).new(
+ registrant=registrant,
+ owner=owner,
+ ociproject=ociproject,
+ ociproject_default=False,
+ require_virtualized=False)
+ self.assertEqual(target.registrant, registrant)
+ self.assertEqual(target.owner, owner)
+ self.assertEqual(target.ociproject, ociproject)
+ self.assertEqual(target.ociproject_default, False)
+ self.assertEqual(target.require_virtualized, False)
diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
index c28eb8b..0eb88af 100644
--- a/lib/lp/registry/tests/test_product.py
+++ b/lib/lp/registry/tests/test_product.py
@@ -126,6 +126,7 @@ from lp.translations.interfaces.translations import (
TranslationsBranchImportMode,
)
+
PRIVATE_PROJECT_TYPES = [InformationType.PROPRIETARY]
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 66c7f73..4e586d1 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -112,6 +112,8 @@ from lp.hardwaredb.interfaces.hwdb import (
IHWSubmissionDevice,
IHWVendorID,
)
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.registry.enums import PersonVisibility
from lp.registry.interfaces.announcement import IAnnouncement
from lp.registry.interfaces.distribution import IDistribution
@@ -3460,3 +3462,13 @@ class EditOCIProjectSeries(AuthorizationBase):
"""Maintainers, drivers, and admins can drive projects."""
return (user.in_admin or
user.isDriver(self.obj.ociproject.pillar))
+
+
+class ViewOCIRecipe(AnonymousAuthorization):
+ """Anyone can view an `IOCIRecipe`."""
+ usedfor = IOCIRecipe
+
+
+class ViewOCIRecipeBuild(AnonymousAuthorization):
+ """Anyone can view an `IOCIRecipe`."""
+ usedfor = IOCIRecipeBuild
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 39d2725..f04596e 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -158,6 +158,11 @@ from lp.hardwaredb.interfaces.hwdb import (
IHWSubmissionDeviceSet,
IHWSubmissionSet,
)
+from lp.oci.model.ocirecipe import (
+ OCIRecipe,
+ OCIRecipeArch,
+ )
+from lp.oci.model.ocirecipechannel import OCIRecipeChannel
from lp.registry.enums import (
BranchSharingPolicy,
BugSharingPolicy,
@@ -4935,6 +4940,46 @@ class BareLaunchpadObjectFactory(ObjectFactory):
oci_project = self.makeOCIProject(**kwargs)
return oci_project.newSeries(name, summary, registrant)
+ def makeOCIRecipe(self, registrant=None, owner=None, ociproject=None,
+ ociproject_default=False, require_virtualized=True):
+ """Make a new OCIRecipe."""
+ if registrant is None:
+ registrant = self.makePerson()
+ if owner is None:
+ owner = self.makeTeam(members=[registrant])
+ if ociproject is None:
+ ociproject = self.makeOCIProject()
+ ocirecipe = OCIRecipe(
+ registrant=registrant,
+ owner=owner,
+ ociproject=ociproject,
+ ociproject_default=ociproject_default,
+ require_virtualized=require_virtualized)
+ return ocirecipe
+
+ def makeOCIRecipeChannel(self, recipe=None, name=None, git_path=None,
+ build_file=None):
+ """Make a new OCIRecipeChannel."""
+ if recipe is None:
+ recipe = self.makeOCIRecipe()
+ if name is None:
+ name = self.getUniqueString(u"oci-recipe-channel-name")
+ if git_path is None:
+ git_path = self.getUniqueString(u"oci-recipe-channel-git-path")
+ if build_file is None:
+ build_file = self.getUniqueString(u"oci-recipe-channel-build-file")
+ oci_channel = OCIRecipeChannel(recipe, name, git_path, build_file)
+ return oci_channel
+
+ def makeOCIRecipeArch(self, recipe=None, processor=None):
+ """Make a new OCIRecipeArch."""
+ if recipe is None:
+ recipe = self.makeOCIRecipe()
+ if processor is None:
+ processor = self.makeProcessor()
+ oci_arch = OCIRecipeArch(recipe, processor)
+ return oci_arch
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
References