launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24144
Re: [Merge] ~twom/launchpad:concrete-oci-projects into launchpad:master
Diff comments:
> 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"
I don't think any of the browser, webservice, or xmlrpc namespaces are needed here at the moment.
> + i18n_domain="launchpad">
> +
> + <!-- ocirecipe -->
Should have standard capitalisation.
> + <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">
OCIBUILD in lp.buildmaster.enums but OCIRECIPEBUILD here; perhaps these should match?
> + <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
> + </securedutility>
> +
> +</configure>
> 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):
I'd rather this be OCIRecipeBuildAlreadyPending, I think.
> + """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.")
"... of this OCI recipe"
> +
> +
> +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."""
"... this OCI recipe"
> +
> +
> +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)
This could use a description explaining what that means; you could borrow the comment from the DB patch.
> +
> + 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/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 "
recipe's
> + "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/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)
This is too broad: we need to be able to dispatch concurrent builds for different architectures. The unique DB constraint is on OCIRecipeBuild (recipe, channel_name, processor, status), so you should follow that.
> + 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/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."""
Looks like copy-and-paste from elsewhere.
> +
> +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/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]
>
>
Revert this bit.
> 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
> @@ -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")
I might be inclined to make git_path and build_file slightly more realistic.
> + 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
--
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.
References