launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28854
[Merge] ~cjwatson/launchpad:black-oci into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:black-oci into launchpad:master.
Commit message:
lp.oci: Apply black
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/427157
--
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:black-oci into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index f667459..1166ab6 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -76,3 +76,5 @@ a6bed71f3d2fdbceae20c2d435c993e8bededdce
ed7d7b97b8fb4ebe92799f922b0fa9c4bd1714e8
# apply black to lp.coop
1e6ead9387a1d073eea9271fe8dc646bb22fa3d2
+# apply black to lp.oci
+06b1048510a0b65bb0a6fc46d4e063510b52b5f6
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a7d8233..43f0476 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -52,6 +52,7 @@ repos:
|code
|codehosting
|coop
+ |oci
)/
- repo: https://github.com/PyCQA/isort
rev: 5.9.2
@@ -80,6 +81,7 @@ repos:
|code
|codehosting
|coop
+ |oci
)/
- id: isort
alias: isort-black
@@ -98,6 +100,7 @@ repos:
|code
|codehosting
|coop
+ |oci
)/
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
diff --git a/lib/lp/oci/browser/hasocirecipes.py b/lib/lp/oci/browser/hasocirecipes.py
index a32ff79..efb92aa 100644
--- a/lib/lp/oci/browser/hasocirecipes.py
+++ b/lib/lp/oci/browser/hasocirecipes.py
@@ -4,8 +4,8 @@
"""Mixins for browser classes for objects related to OCI recipe."""
__all__ = [
- 'HasOCIRecipesMenuMixin',
- ]
+ "HasOCIRecipesMenuMixin",
+]
from zope.component import getUtility
@@ -17,8 +17,11 @@ class HasOCIRecipesMenuMixin:
"""A mixin for context menus for objects that has OCI recipes."""
def view_oci_recipes(self):
- target = '+oci-recipes'
- text = 'View OCI recipes'
- enabled = not getUtility(IOCIRecipeSet).findByContext(
- self.context, visible_by_user=self.user).is_empty()
- return Link(target, text, enabled=enabled, icon='info')
+ target = "+oci-recipes"
+ text = "View OCI recipes"
+ enabled = (
+ not getUtility(IOCIRecipeSet)
+ .findByContext(self.context, visible_by_user=self.user)
+ .is_empty()
+ )
+ return Link(target, text, enabled=enabled, icon="info")
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 7329652..0b9f4d2 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -4,22 +4,19 @@
"""OCI recipe views."""
__all__ = [
- 'OCIRecipeAddView',
- 'OCIRecipeAdminView',
- 'OCIRecipeContextMenu',
- 'OCIRecipeDeleteView',
- 'OCIRecipeEditPushRulesView',
- 'OCIRecipeEditView',
- 'OCIRecipeNavigation',
- 'OCIRecipeNavigationMenu',
- 'OCIRecipeRequestBuildsView',
- 'OCIRecipeView',
- ]
-
-from lazr.restful.interface import (
- copy_field,
- use_template,
- )
+ "OCIRecipeAddView",
+ "OCIRecipeAdminView",
+ "OCIRecipeContextMenu",
+ "OCIRecipeDeleteView",
+ "OCIRecipeEditPushRulesView",
+ "OCIRecipeEditView",
+ "OCIRecipeNavigation",
+ "OCIRecipeNavigationMenu",
+ "OCIRecipeRequestBuildsView",
+ "OCIRecipeView",
+]
+
+from lazr.restful.interface import copy_field, use_template
from zope.component import getUtility
from zope.formlib.form import FormFields
from zope.formlib.textwidgets import TextAreaWidget
@@ -27,7 +24,7 @@ from zope.formlib.widget import (
CustomWidgetFactory,
DisplayWidget,
renderElement,
- )
+)
from zope.interface import Interface
from zope.schema import (
Bool,
@@ -37,66 +34,63 @@ from zope.schema import (
Text,
TextLine,
ValidationError,
- )
+)
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
- )
+ action,
+)
from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
-from lp.app.browser.tales import (
- format_link,
- GitRepositoryFormatterAPI,
- )
+from lp.app.browser.tales import GitRepositoryFormatterAPI, format_link
from lp.app.errors import UnexpectedFormData
from lp.app.validators.validation import validate_oci_branch_name
from lp.app.vocabularies import InformationTypeVocabulary
from lp.app.widgets.itemswidgets import (
LabeledMultiCheckBoxWidget,
LaunchpadRadioWidgetWithDescription,
- )
+)
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.browser.widgets.gitref import GitRefWidget
from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.oci.interfaces.ocipushrule import (
IOCIPushRuleSet,
OCIPushRuleAlreadyExists,
- )
+)
from lp.oci.interfaces.ocirecipe import (
+ OCI_RECIPE_ALLOW_CREATE,
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
IOCIRecipe,
IOCIRecipeSet,
NoSuchOCIRecipe,
- OCI_RECIPE_ALLOW_CREATE,
- OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
OCIRecipeFeatureDisabled,
- )
+)
from lp.oci.interfaces.ocirecipebuild import (
IOCIRecipeBuildSet,
OCIRecipeBuildRegistryUploadStatus,
- )
+)
from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
from lp.oci.interfaces.ociregistrycredentials import (
IOCIRegistryCredentialsSet,
OCIRegistryCredentialsAlreadyExist,
user_can_edit_credentials_for_owner,
- )
+)
from lp.registry.interfaces.person import IPersonSet
from lp.services.features import getFeatureFlag
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
- canonical_url,
ContextMenu,
- enabled_with_permission,
LaunchpadView,
Link,
Navigation,
NavigationMenu,
+ canonical_url,
+ enabled_with_permission,
stepthrough,
structured,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.breadcrumb import NameBreadcrumb
@@ -110,7 +104,7 @@ class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
usedfor = IOCIRecipe
- @stepthrough('+build-request')
+ @stepthrough("+build-request")
def traverse_build_request(self, name):
try:
job_id = int(name)
@@ -118,14 +112,14 @@ class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
return None
return self.context.getBuildRequest(job_id)
- @stepthrough('+build')
+ @stepthrough("+build")
def traverse_build(self, name):
build = get_build_by_id_str(IOCIRecipeBuildSet, name)
if build is None or build.recipe != self.context:
return None
return build
- @stepthrough('+push-rule')
+ @stepthrough("+push-rule")
def traverse_pushrule(self, id):
id = int(id)
return getUtility(IOCIPushRuleSet).getByID(id)
@@ -139,7 +133,6 @@ class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
class OCIRecipeBreadcrumb(NameBreadcrumb):
-
@property
def inside(self):
return self.context.oci_project
@@ -162,11 +155,14 @@ class OCIRecipeNavigationMenu(NavigationMenu):
def edit(self):
return Link("+edit", "Edit OCI recipe", icon="edit")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def webhooks(self):
return Link(
- '+webhooks', 'Manage webhooks', icon='edit',
- enabled=bool(getFeatureFlag(OCI_RECIPE_WEBHOOKS_FEATURE_FLAG)))
+ "+webhooks",
+ "Manage webhooks",
+ icon="edit",
+ enabled=bool(getFeatureFlag(OCI_RECIPE_WEBHOOKS_FEATURE_FLAG)),
+ )
@enabled_with_permission("launchpad.Edit")
def delete(self):
@@ -178,19 +174,22 @@ class OCIRecipeContextMenu(ContextMenu):
usedfor = IOCIRecipe
- facet = 'overview'
+ facet = "overview"
- links = ('request_builds', 'edit_push_rules',
- 'add_subscriber', 'subscription')
+ links = (
+ "request_builds",
+ "edit_push_rules",
+ "add_subscriber",
+ "subscription",
+ )
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def request_builds(self):
- return Link('+request-builds', 'Request builds', icon='add')
+ return Link("+request-builds", "Request builds", icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def edit_push_rules(self):
- return Link(
- '+edit-push-rules', 'Edit push rules', icon='edit')
+ return Link("+edit-push-rules", "Edit push rules", icon="edit")
@enabled_with_permission("launchpad.AnyPerson")
def subscription(self):
@@ -214,11 +213,12 @@ class OCIRecipeListingView(LaunchpadView):
"""Default view for the list of OCI recipes of a context (OCI project
or Person).
"""
- page_title = 'Recipes'
+
+ page_title = "Recipes"
@property
def label(self):
- return 'OCI recipes for %s' % self.context.name
+ return "OCI recipes for %s" % self.context.name
@property
def title(self):
@@ -227,8 +227,9 @@ class OCIRecipeListingView(LaunchpadView):
@property
def recipes(self):
recipes = getUtility(IOCIRecipeSet).findByContext(
- self.context, visible_by_user=self.user)
- return recipes.order_by('name')
+ self.context, visible_by_user=self.user
+ )
+ return recipes.order_by("name")
@property
def recipes_navigator(self):
@@ -266,8 +267,7 @@ class OCIRecipeView(LaunchpadView):
# We're still not sure how this plays into
# plans for internal registry announcements and such, so we
# land redaction first and think about this later.
- return list(
- getUtility(IOCIPushRuleSet).findByRecipe(self.context))
+ return list(getUtility(IOCIPushRuleSet).findByRecipe(self.context))
@property
def has_push_rules(self):
@@ -284,10 +284,15 @@ class OCIRecipeView(LaunchpadView):
def person_picker(self):
field = copy_field(
IOCIRecipe["owner"],
- vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay")
+ vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay",
+ )
return InlinePersonEditPickerWidget(
- self.context, field, format_link(self.context.owner),
- header="Change owner", step_title="Select a new owner")
+ self.context,
+ field,
+ format_link(self.context.owner),
+ header="Change owner",
+ step_title="Select a new owner",
+ )
@property
def build_frequency(self):
@@ -300,11 +305,12 @@ class OCIRecipeView(LaunchpadView):
def build_args(self):
return "\n".join(
"%s=%s" % (k, v)
- for k, v in sorted(self.context.build_args.items()))
+ for k, v in sorted(self.context.build_args.items())
+ )
@property
def distribution_has_credentials(self):
- if hasattr(self.context, 'oci_project'):
+ if hasattr(self.context, "oci_project"):
oci_project = self.context.oci_project
else:
oci_project = self.context
@@ -313,13 +319,12 @@ class OCIRecipeView(LaunchpadView):
def getImageForStatus(self, status):
image_map = {
- BuildSetStatus.NEEDSBUILD: '/@@/build-needed',
- BuildSetStatus.FULLYBUILT_PENDING: '/@@/build-success-publishing',
- BuildSetStatus.FAILEDTOBUILD: '/@@/no',
- BuildSetStatus.BUILDING: '/@@/processing',
- }
- return image_map.get(
- status, '/@@/yes')
+ BuildSetStatus.NEEDSBUILD: "/@@/build-needed",
+ BuildSetStatus.FULLYBUILT_PENDING: "/@@/build-success-publishing",
+ BuildSetStatus.FAILEDTOBUILD: "/@@/no",
+ BuildSetStatus.BUILDING: "/@@/processing",
+ }
+ return image_map.get(status, "/@@/yes")
def _convertBuildJobToStatus(self, build_job):
recipe_set = getUtility(IOCIRecipeSet)
@@ -327,7 +332,8 @@ class OCIRecipeView(LaunchpadView):
upload_status = build_job.registry_upload_status
# This is just a dict, but zope wraps it as RecipeSet is secured
status = removeSecurityProxy(
- recipe_set.getStatusSummaryForBuilds([build_job]))
+ recipe_set.getStatusSummaryForBuilds([build_job])
+ )
# Add the registry job status
status["upload_requested"] = upload_status != unscheduled_upload
status["upload"] = upload_status
@@ -338,7 +344,7 @@ class OCIRecipeView(LaunchpadView):
"job_id": "build{}".format(build_job.id),
"date_created": build_job.date_created,
"date_finished": build_job.date_finished,
- "build_status": status
+ "build_status": status,
}
def build_requests(self):
@@ -352,7 +358,7 @@ class OCIRecipeView(LaunchpadView):
if len(build_requests) < 10:
recipe = self.context
no_request_builds = recipe.completed_builds_without_build_request
- for build in no_request_builds[:10 - len(build_requests)]:
+ for build in no_request_builds[: 10 - len(build_requests)]:
build_requests.append(self._convertBuildJobToStatus(build))
return build_requests[:10]
@@ -372,7 +378,7 @@ def builds_for_recipe(recipe):
"""
builds = list(recipe.pending_builds)
if len(builds) < 10:
- builds.extend(recipe.completed_builds[:10 - len(builds)])
+ builds.extend(recipe.completed_builds[: 10 - len(builds)])
return builds
@@ -408,8 +414,7 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
@cachedproperty
def push_rules(self):
- return list(
- getUtility(IOCIPushRuleSet).findByRecipe(self.context))
+ return list(getUtility(IOCIPushRuleSet).findByRecipe(self.context))
@property
def has_push_rules(self):
@@ -418,7 +423,8 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
@cachedproperty
def can_edit_credentials(self):
return user_can_edit_credentials_for_owner(
- self.context.owner, self.user)
+ self.context.owner, self.user
+ )
def _getFieldName(self, name, rule_id):
"""Get the combined field name for an `OCIPushRule` ID.
@@ -437,13 +443,15 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
field_bits = field_name.split(".")
if len(field_bits) != 2:
raise UnexpectedFormData(
- "Cannot parse field name: %s" % field_name)
+ "Cannot parse field name: %s" % field_name
+ )
field_type = field_bits[0]
try:
rule_id = int(field_bits[1])
except ValueError:
raise UnexpectedFormData(
- "Cannot parse field name: %s" % field_name)
+ "Cannot parse field name: %s" % field_name
+ )
return field_type, rule_id
def setUpFields(self):
@@ -460,26 +468,38 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
for elem in list(self.context.push_rules):
image_name_fields.append(
TextLine(
- __name__=self._getFieldName('image_name', elem.id),
+ __name__=self._getFieldName("image_name", elem.id),
default=elem.image_name,
- required=True, readonly=False))
- if check_permission('launchpad.View', elem.registry_credentials):
+ required=True,
+ readonly=False,
+ )
+ )
+ if check_permission("launchpad.View", elem.registry_credentials):
url_fields.append(
TextLine(
- __name__=self._getFieldName('url', elem.id),
+ __name__=self._getFieldName("url", elem.id),
default=elem.registry_credentials.url,
- required=True, readonly=True))
+ required=True,
+ readonly=True,
+ )
+ )
region = elem.registry_credentials.region
region_fields.append(
TextLine(
- __name__=self._getFieldName('region', elem.id),
+ __name__=self._getFieldName("region", elem.id),
default=region,
- required=False, readonly=True))
+ required=False,
+ readonly=True,
+ )
+ )
username_fields.append(
TextLine(
- __name__=self._getFieldName('username', elem.id),
+ __name__=self._getFieldName("username", elem.id),
default=elem.registry_credentials.username,
- required=True, readonly=True))
+ required=True,
+ readonly=True,
+ )
+ )
else:
# XXX cjwatson 2020-08-27: Ideally we'd be able to just show
# the URL, and maybe the username too, but the
@@ -492,71 +512,89 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
# viewer's recipes.
private_url_fields.append(
TextLine(
- __name__=self._getFieldName('url', elem.id),
- default='', required=True, readonly=True))
+ __name__=self._getFieldName("url", elem.id),
+ default="",
+ required=True,
+ readonly=True,
+ )
+ )
private_region_fields.append(
TextLine(
- __name__=self._getFieldName('region', elem.id),
- default='', required=False, readonly=True))
+ __name__=self._getFieldName("region", elem.id),
+ default="",
+ required=False,
+ readonly=True,
+ )
+ )
private_username_fields.append(
TextLine(
- __name__=self._getFieldName('username', elem.id),
- default='', required=True, readonly=True))
+ __name__=self._getFieldName("username", elem.id),
+ default="",
+ required=True,
+ readonly=True,
+ )
+ )
delete_fields.append(
Bool(
- __name__=self._getFieldName('delete', elem.id),
+ __name__=self._getFieldName("delete", elem.id),
default=False,
- required=True, readonly=False))
+ required=True,
+ readonly=False,
+ )
+ )
image_name_fields.append(
- TextLine(
- __name__='add_image_name',
- required=False, readonly=False))
+ TextLine(__name__="add_image_name", required=False, readonly=False)
+ )
add_credentials = Choice(
- __name__='add_credentials',
- default='existing', values=('existing', 'new'),
- required=False, readonly=False)
+ __name__="add_credentials",
+ default="existing",
+ values=("existing", "new"),
+ required=False,
+ readonly=False,
+ )
existing_credentials = Choice(
- vocabulary='OCIRegistryCredentials',
+ vocabulary="OCIRegistryCredentials",
required=False,
- __name__='existing_credentials')
+ __name__="existing_credentials",
+ )
url_fields.append(
- TextLine(
- __name__='add_url',
- required=False, readonly=False))
+ TextLine(__name__="add_url", required=False, readonly=False)
+ )
region_fields.append(
- TextLine(
- __name__='add_region',
- required=False, readonly=False))
+ TextLine(__name__="add_region", required=False, readonly=False)
+ )
username_fields.append(
- TextLine(
- __name__='add_username',
- required=False, readonly=False))
+ TextLine(__name__="add_username", required=False, readonly=False)
+ )
password_fields.append(
- Password(
- __name__='add_password',
- required=False, readonly=False))
+ Password(__name__="add_password", required=False, readonly=False)
+ )
password_fields.append(
Password(
- __name__='add_confirm_password',
- required=False, readonly=False))
+ __name__="add_confirm_password", required=False, readonly=False
+ )
+ )
self.form_fields = (
- FormFields(*image_name_fields) +
- FormFields(*url_fields) +
- FormFields(
- *private_url_fields,
- custom_widget=InvisibleCredentialsWidget) +
- FormFields(*region_fields) +
- FormFields(
+ FormFields(*image_name_fields)
+ + FormFields(*url_fields)
+ + FormFields(
+ *private_url_fields, custom_widget=InvisibleCredentialsWidget
+ )
+ + FormFields(*region_fields)
+ + FormFields(
*private_region_fields,
- custom_widget=InvisibleCredentialsWidget) +
- FormFields(*username_fields) +
- FormFields(
+ custom_widget=InvisibleCredentialsWidget,
+ )
+ + FormFields(*username_fields)
+ + FormFields(
*private_username_fields,
- custom_widget=InvisibleCredentialsWidget) +
- FormFields(*password_fields) +
- FormFields(*delete_fields) +
- FormFields(add_credentials, existing_credentials))
+ custom_widget=InvisibleCredentialsWidget,
+ )
+ + FormFields(*password_fields)
+ + FormFields(*delete_fields)
+ + FormFields(add_credentials, existing_credentials)
+ )
def setUpWidgets(self, context=None):
"""See `LaunchpadFormView`."""
@@ -567,9 +605,9 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
@property
def label(self):
- return 'Edit OCI push rules for %s' % self.context.name
+ return "Edit OCI push rules for %s" % self.context.name
- page_title = 'Edit OCI push rules'
+ page_title = "Edit OCI push rules"
@property
def cancel_url(self):
@@ -577,16 +615,13 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
def getRuleWidgets(self, rule):
widgets_by_name = {widget.name: widget for widget in self.widgets}
- url_field_name = (
- "field." + self._getFieldName("url", rule.id))
- region_field_name = (
- "field." + self._getFieldName("region", rule.id))
- image_field_name = (
- "field." + self._getFieldName("image_name", rule.id))
- username_field_name = (
- "field." + self._getFieldName("username", rule.id))
- delete_field_name = (
- "field." + self._getFieldName("delete", rule.id))
+ url_field_name = "field." + self._getFieldName("url", rule.id)
+ region_field_name = "field." + self._getFieldName("region", rule.id)
+ image_field_name = "field." + self._getFieldName("image_name", rule.id)
+ username_field_name = "field." + self._getFieldName(
+ "username", rule.id
+ )
+ delete_field_name = "field." + self._getFieldName("delete", rule.id)
return {
"url": widgets_by_name[url_field_name],
"region": widgets_by_name[region_field_name],
@@ -599,14 +634,15 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
widgets_by_name = {widget.name: widget for widget in self.widgets}
return {
"image_name": widgets_by_name["field.add_image_name"],
- "existing_credentials":
- widgets_by_name["field.existing_credentials"],
+ "existing_credentials": widgets_by_name[
+ "field.existing_credentials"
+ ],
"url": widgets_by_name["field.add_url"],
"region": widgets_by_name["field.add_region"],
"username": widgets_by_name["field.add_username"],
"password": widgets_by_name["field.add_password"],
"confirm_password": widgets_by_name["field.add_confirm_password"],
- }
+ }
def parseData(self, data):
"""Rearrange form data to make it easier to process."""
@@ -620,23 +656,33 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
add_existing_credentials = data.get("existing_credentials")
# parse data from the Add new rule section of the form
- if (add_url or add_region or add_username or add_password or
- add_confirm_password or add_image_name or
- add_existing_credentials):
- parsed_data.setdefault(None, {
- "image_name": add_image_name,
- "url": add_url,
- "region": add_region,
- "username": add_username,
- "password": add_password,
- "confirm_password": add_confirm_password,
- "existing_credentials": data["existing_credentials"],
- "add_credentials": data["add_credentials"],
- "action": "add",
- })
+ if (
+ add_url
+ or add_region
+ or add_username
+ or add_password
+ or add_confirm_password
+ or add_image_name
+ or add_existing_credentials
+ ):
+ parsed_data.setdefault(
+ None,
+ {
+ "image_name": add_image_name,
+ "url": add_url,
+ "region": add_region,
+ "username": add_username,
+ "password": add_password,
+ "confirm_password": add_confirm_password,
+ "existing_credentials": data["existing_credentials"],
+ "add_credentials": data["add_credentials"],
+ "action": "add",
+ },
+ )
# parse data from the Edit existing rule section of the form
for field_name in sorted(
- name for name in data if name.split(".")[0] == "image_name"):
+ name for name in data if name.split(".")[0] == "image_name"
+ ):
_, rule_id = self._parseFieldName(field_name)
image_field_name = self._getFieldName("image_name", rule_id)
delete_field_name = self._getFieldName("delete", rule_id)
@@ -644,10 +690,13 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
action = "delete"
else:
action = "change"
- parsed_data.setdefault(rule_id, {
- "image_name": data.get(image_field_name),
- "action": action,
- })
+ parsed_data.setdefault(
+ rule_id,
+ {
+ "image_name": data.get(image_field_name),
+ "action": action,
+ },
+ )
return parsed_data
@@ -678,22 +727,27 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
return
if password != confirm_password:
self.setFieldError(
- "add_confirm_password", "Passwords do not match.")
+ "add_confirm_password", "Passwords do not match."
+ )
return
credentials_set = getUtility(IOCIRegistryCredentialsSet)
try:
- credential_data = {'username': username, 'password': password}
+ credential_data = {"username": username, "password": password}
if region is not None:
- credential_data['region'] = region
+ credential_data["region"] = region
credentials = credentials_set.getOrCreate(
- registrant=self.user, owner=self.context.owner, url=url,
- credentials=credential_data)
+ registrant=self.user,
+ owner=self.context.owner,
+ url=url,
+ credentials=credential_data,
+ )
except OCIRegistryCredentialsAlreadyExist:
self.setFieldError(
"add_url",
"Credentials already exist with the same URL, username, "
- "and region.")
+ "and region.",
+ )
return
except ValidationError:
self.setFieldError("add_url", "Not a valid URL.")
@@ -701,17 +755,17 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
try:
getUtility(IOCIPushRuleSet).new(
- self.context, credentials, image_name)
+ self.context, credentials, image_name
+ )
except OCIPushRuleAlreadyExists:
self.setFieldError(
"add_image_name",
"A push rule already exists with the same URL, image name, "
- "and credentials.")
+ "and credentials.",
+ )
def updatePushRulesFromData(self, parsed_data):
- rules_map = {
- rule.id: rule
- for rule in self.context.push_rules}
+ rules_map = {rule.id: rule for rule in self.context.push_rules}
for rule_id, parsed_rules in parsed_data.items():
rule = rules_map.get(rule_id)
action = parsed_rules["action"]
@@ -720,9 +774,9 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
image_name = parsed_rules["image_name"]
if not image_name:
self.setFieldError(
- self._getFieldName(
- "image_name", rule_id),
- "Image name must be set.")
+ self._getFieldName("image_name", rule_id),
+ "Image name must be set.",
+ )
else:
if rule.image_name != image_name:
rule.setNewImageName(image_name)
@@ -748,16 +802,18 @@ class OCIRecipeRequestBuildsView(LaunchpadFormView):
@property
def label(self):
- return 'Request builds for %s' % self.context.name
+ return "Request builds for %s" % self.context.name
- page_title = 'Request builds'
+ page_title = "Request builds"
class schema(Interface):
"""Schema for requesting a build."""
distro_arch_series = List(
- Choice(vocabulary='OCIRecipeDistroArchSeries'),
- title='Architectures', required=True)
+ Choice(vocabulary="OCIRecipeDistroArchSeries"),
+ title="Architectures",
+ required=True,
+ )
custom_widget_distro_arch_series = LabeledMultiCheckBoxWidget
@@ -768,61 +824,71 @@ class OCIRecipeRequestBuildsView(LaunchpadFormView):
@property
def initial_values(self):
"""See `LaunchpadFormView`."""
- return {'distro_arch_series': self.context.getAllowedArchitectures()}
+ return {"distro_arch_series": self.context.getAllowedArchitectures()}
def validate(self, data):
"""See `LaunchpadFormView`."""
- arches = data.get('distro_arch_series', [])
+ arches = data.get("distro_arch_series", [])
if not arches:
self.setFieldError(
- 'distro_arch_series',
- 'You need to select at least one architecture.')
+ "distro_arch_series",
+ "You need to select at least one architecture.",
+ )
- @action('Request builds', name='request')
+ @action("Request builds", name="request")
def request_action(self, action, data):
- if data.get('distro_arch_series'):
+ if data.get("distro_arch_series"):
architectures = [
- arch.architecturetag for arch in data['distro_arch_series']]
+ arch.architecturetag for arch in data["distro_arch_series"]
+ ]
else:
architectures = None
self.context.requestBuilds(self.user, architectures)
self.request.response.addNotification(
"Your builds were scheduled and should start soon. "
- "Refresh this page for details.")
+ "Refresh this page for details."
+ )
self.next_url = self.cancel_url
class IOCIRecipeEditSchema(Interface):
"""Schema for adding or editing an OCI recipe."""
- use_template(IOCIRecipe, include=[
- "name",
- "owner",
- "information_type",
- "description",
- "git_ref",
- "build_file",
- "build_args",
- "build_path",
- "build_daily",
- "require_virtualized",
- "allow_internet",
- ])
+ use_template(
+ IOCIRecipe,
+ include=[
+ "name",
+ "owner",
+ "information_type",
+ "description",
+ "git_ref",
+ "build_file",
+ "build_args",
+ "build_path",
+ "build_daily",
+ "require_virtualized",
+ "allow_internet",
+ ],
+ )
class OCIRecipeFormMixin:
"""Mixin with common processing for both edit and add views."""
+
custom_widget_build_args = CustomWidgetFactory(
- TextAreaWidget, height=5, width=100)
+ TextAreaWidget, height=5, width=100
+ )
custom_widget_information_type = CustomWidgetFactory(
LaunchpadRadioWidgetWithDescription,
- vocabulary=InformationTypeVocabulary(types=[]))
+ vocabulary=InformationTypeVocabulary(types=[]),
+ )
def setUpInformationTypeWidget(self):
- info_type_widget = self.widgets['information_type']
+ info_type_widget = self.widgets["information_type"]
info_type_widget.vocabulary = InformationTypeVocabulary(
- types=self.getInformationTypesToShow())
+ types=self.getInformationTypesToShow()
+ )
def getInformationTypesToShow(self):
"""Get the information types to display on the edit form.
@@ -840,36 +906,45 @@ class OCIRecipeFormMixin:
if IOCIRecipe.providedBy(self.context):
default = "\n".join(
"%s=%s" % (k, v)
- for k, v in sorted(self.context.build_args.items()))
+ for k, v in sorted(self.context.build_args.items())
+ )
else:
default = ""
- return FormFields(Text(
- __name__='build_args',
- title='Build-time ARG variables',
- description=("One per line. Each ARG should be in the format "
- "of ARG_KEY=arg_value."),
- default=default,
- required=False, readonly=False))
+ return FormFields(
+ Text(
+ __name__="build_args",
+ title="Build-time ARG variables",
+ description=(
+ "One per line. Each ARG should be in the format "
+ "of ARG_KEY=arg_value."
+ ),
+ default=default,
+ required=False,
+ readonly=False,
+ )
+ )
def validateBuildArgs(self, data):
- field_value = data.get('build_args')
+ field_value = data.get("build_args")
if not field_value:
return
build_args = {}
for i, line in enumerate(field_value.split("\n")):
- if '=' not in line:
- msg = ("'%s' at line %s is not a valid KEY=value pair." %
- (line, i + 1))
+ if "=" not in line:
+ msg = "'%s' at line %s is not a valid KEY=value pair." % (
+ line,
+ i + 1,
+ )
self.setFieldError("build_args", str(msg))
return
- k, v = line.split('=', 1)
+ k, v = line.split("=", 1)
build_args[k] = v
- data['build_args'] = build_args
+ data["build_args"] = build_args
def userIsRecipeAdmin(self):
if check_permission("launchpad.Admin", self.context):
return True
- person = getattr(self.request.principal, 'person', None)
+ person = getattr(self.request.principal, "person", None)
if not person:
return False
# Edit context = OCIRecipe, New context = OCIProject
@@ -890,7 +965,7 @@ class OCIRecipeFormMixin:
@property
def distribution_has_credentials(self):
- if hasattr(self.context, 'oci_project'):
+ if hasattr(self.context, "oci_project"):
oci_project = self.context.oci_project
else:
oci_project = self.context
@@ -898,8 +973,9 @@ class OCIRecipeFormMixin:
return bool(distro and distro.oci_registry_credentials)
-class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
- OCIRecipeFormMixin):
+class OCIRecipeAddView(
+ LaunchpadFormView, EnableProcessorsMixin, OCIRecipeFormMixin
+):
"""View for creating OCI recipes."""
page_title = label = "Create a new OCI recipe"
@@ -914,7 +990,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
"build_file",
"build_path",
"build_daily",
- )
+ )
custom_widget_git_ref = GitRefWidget
def initialize(self):
@@ -930,25 +1006,36 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
getUtility(IProcessorSet).getAll(),
"The architectures that this OCI recipe builds for. Some "
"architectures are restricted and may only be enabled or "
- "disabled by administrators.")
- self.form_fields += FormFields(Bool(
- __name__="official_recipe",
- title="Official recipe",
- description=(
- "Mark this recipe as official for this OCI Project. "
- "Allows use of distribution registry credentials "
- "and the default git repository routing. "
- "May only be enabled by the owner of the OCI Project."),
- default=False,
- required=False, readonly=False))
- if self.distribution_has_credentials:
- self.form_fields += FormFields(TextLine(
- __name__='image_name',
- title="Image name",
+ "disabled by administrators.",
+ )
+ self.form_fields += FormFields(
+ Bool(
+ __name__="official_recipe",
+ title="Official recipe",
description=(
- "Name to use for registry upload. "
- "Defaults to the name of the recipe."),
- required=False, readonly=False))
+ "Mark this recipe as official for this OCI Project. "
+ "Allows use of distribution registry credentials "
+ "and the default git repository routing. "
+ "May only be enabled by the owner of the OCI Project."
+ ),
+ default=False,
+ required=False,
+ readonly=False,
+ )
+ )
+ if self.distribution_has_credentials:
+ self.form_fields += FormFields(
+ TextLine(
+ __name__="image_name",
+ title="Image name",
+ description=(
+ "Name to use for registry upload. "
+ "Defaults to the name of the recipe."
+ ),
+ required=False,
+ readonly=False,
+ )
+ )
def setUpGitRefWidget(self):
"""Setup GitRef widget indicating the user to use the default
@@ -963,13 +1050,15 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
# Do not override more important git_ref errors.
return
default_repo = getUtility(IGitRepositorySet).getDefaultRepository(
- self.context)
+ self.context
+ )
if default_repo is None:
msg = (
- 'The default git repository for this OCI project was not '
- 'created yet.<br/>'
+ "The default git repository for this OCI project was not "
+ "created yet.<br/>"
'Check the <a href="%s">OCI project page</a> for instructions '
- 'on how to create one.')
+ "on how to create one."
+ )
msg = structured(msg, canonical_url(self.context))
self.widget_errors["git_ref"] = msg.escapedtext
@@ -981,7 +1070,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
self.setUpGitRefWidget()
# disable the official recipe button if the user doesn't have
# permissions to change it
- widget = self.widgets['official_recipe']
+ widget = self.widgets["official_recipe"]
if not self.userIsRecipeAdmin():
widget.extra = "disabled='disabled'"
@@ -998,9 +1087,11 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
"build_file": "Dockerfile",
"build_path": ".",
"processors": [
- p for p in getUtility(IProcessorSet).getAll()
- if p.build_by_default],
- }
+ p
+ for p in getUtility(IProcessorSet).getAll()
+ if p.build_by_default
+ ],
+ }
def validate(self, data):
"""See `LaunchpadFormView`."""
@@ -1012,27 +1103,36 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
self.setFieldError(
"name",
"There is already an OCI recipe owned by %s in %s with "
- "this name." % (
- owner.display_name, self.context.display_name))
+ "this name."
+ % (owner.display_name, self.context.display_name),
+ )
self.validateBuildArgs(data)
official = data.get("official_recipe", None)
if official and not self.userIsRecipeAdmin():
self.setFieldError(
"official_recipe",
"You do not have permission to set the official status "
- "of this recipe.")
+ "of this recipe.",
+ )
@action("Create OCI recipe", name="create")
def create_action(self, action, data):
recipe = getUtility(IOCIRecipeSet).new(
- name=data["name"], registrant=self.user, owner=data["owner"],
- oci_project=self.context, git_ref=data["git_ref"],
- build_file=data["build_file"], description=data["description"],
- build_daily=data["build_daily"], build_args=data["build_args"],
- build_path=data["build_path"], processors=data["processors"],
- official=data.get('official_recipe', False),
+ name=data["name"],
+ registrant=self.user,
+ owner=data["owner"],
+ oci_project=self.context,
+ git_ref=data["git_ref"],
+ build_file=data["build_file"],
+ description=data["description"],
+ build_daily=data["build_daily"],
+ build_args=data["build_args"],
+ build_path=data["build_path"],
+ processors=data["processors"],
+ official=data.get("official_recipe", False),
# image_name is only available if using distribution credentials.
- image_name=data.get("image_name"))
+ image_name=data.get("image_name"),
+ )
self.next_url = canonical_url(recipe)
@@ -1055,12 +1155,14 @@ class BaseOCIRecipeEditView(LaunchpadEditFormView):
if new_processors is not None:
if set(self.context.processors) != set(new_processors):
self.context.setProcessors(
- new_processors, check_permissions=True, user=self.user)
+ new_processors, check_permissions=True, user=self.user
+ )
del data["processors"]
- official = data.pop('official_recipe', None)
+ official = data.pop("official_recipe", None)
if official is not None and self.userIsRecipeAdmin():
self.context.oci_project.setOfficialRecipeStatus(
- self.context, official)
+ self.context, official
+ )
self.updateContextFromData(data)
self.next_url = canonical_url(self.context)
@@ -1083,8 +1185,9 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView):
field_names = ("require_virtualized", "allow_internet")
-class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
- OCIRecipeFormMixin):
+class OCIRecipeEditView(
+ BaseOCIRecipeEditView, EnableProcessorsMixin, OCIRecipeFormMixin
+):
"""View for editing OCI recipes."""
@property
@@ -1102,7 +1205,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
"build_file",
"build_path",
"build_daily",
- )
+ )
custom_widget_git_ref = GitRefWidget
def setUpGitRefWidget(self):
@@ -1119,17 +1222,21 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
return
msg = None
if self.context.git_ref.namespace.target != self.context.oci_project:
- msg = ("This recipe's git repository is not in the "
- "correct namespace.<br/>")
+ msg = (
+ "This recipe's git repository is not in the "
+ "correct namespace.<br/>"
+ )
default_repo = getUtility(IGitRepositorySet).getDefaultRepository(
- oci_proj)
+ oci_proj
+ )
if default_repo:
- link = GitRepositoryFormatterAPI(default_repo).link('')
+ link = GitRepositoryFormatterAPI(default_repo).link("")
msg += "Consider using %s instead." % link
else:
msg += (
'Check the <a href="%(oci_proj_url)s">OCI project page</a>'
- ' for instructions on how to create it correctly.')
+ " for instructions on how to create it correctly."
+ )
if msg:
msg = structured(msg, oci_proj_url=oci_proj_url)
self.widget_errors["git_ref"] = msg.escapedtext
@@ -1141,7 +1248,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
self.setUpGitRefWidget()
# disable the official recipe button if the user doesn't have
# permissions to change it
- widget = self.widgets['official_recipe']
+ widget = self.widgets["official_recipe"]
if not self.userIsRecipeAdmin():
widget.extra = "disabled='disabled'"
@@ -1153,26 +1260,37 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
self.context.available_processors,
"The architectures that this OCI recipe builds for. Some "
"architectures are restricted and may only be enabled or "
- "disabled by administrators.")
- self.form_fields += FormFields(Bool(
- __name__="official_recipe",
- title="Official recipe",
- description=(
- "Mark this recipe as official for this OCI Project. "
- "Allows use of distribution registry credentials "
- "and the default git repository routing. "
- "May only be enabled by the owner of the OCI Project."),
- default=self.context.official,
- required=False, readonly=False))
- if self.distribution_has_credentials:
- self.form_fields += FormFields(TextLine(
- __name__='image_name',
- title="Image name",
+ "disabled by administrators.",
+ )
+ self.form_fields += FormFields(
+ Bool(
+ __name__="official_recipe",
+ title="Official recipe",
description=(
- "Name to use for registry upload. "
- "Defaults to the name of the recipe."),
- default=self.context.image_name,
- required=False, readonly=False))
+ "Mark this recipe as official for this OCI Project. "
+ "Allows use of distribution registry credentials "
+ "and the default git repository routing. "
+ "May only be enabled by the owner of the OCI Project."
+ ),
+ default=self.context.official,
+ required=False,
+ readonly=False,
+ )
+ )
+ if self.distribution_has_credentials:
+ self.form_fields += FormFields(
+ TextLine(
+ __name__="image_name",
+ title="Image name",
+ description=(
+ "Name to use for registry upload. "
+ "Defaults to the name of the recipe."
+ ),
+ default=self.context.image_name,
+ required=False,
+ readonly=False,
+ )
+ )
def validate(self, data):
"""See `LaunchpadFormView`."""
@@ -1184,14 +1302,18 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
if owner and name:
try:
recipe = getUtility(IOCIRecipeSet).getByName(
- owner, self.context.oci_project, name)
+ owner, self.context.oci_project, name
+ )
if recipe != self.context:
self.setFieldError(
"name",
"There is already an OCI recipe owned by %s in %s "
- "with this name." % (
+ "with this name."
+ % (
owner.display_name,
- self.context.oci_project.display_name))
+ self.context.oci_project.display_name,
+ ),
+ )
except NoSuchOCIRecipe:
pass
if "processors" in data:
@@ -1208,14 +1330,15 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
# enabled. Leave it untouched.
data["processors"].append(processor)
self.validateBuildArgs(data)
- official = data.get('official_recipe')
+ official = data.get("official_recipe")
official_change = self.context.official != official
is_admin = self.userIsRecipeAdmin()
if official is not None and official_change and not is_admin:
self.setFieldError(
"official_recipe",
"You do not have permission to change the official status "
- "of this recipe.")
+ "of this recipe.",
+ )
class OCIRecipeDeleteView(BaseOCIRecipeEditView):
diff --git a/lib/lp/oci/browser/ocirecipebuild.py b/lib/lp/oci/browser/ocirecipebuild.py
index 9f1d55e..7aaad43 100644
--- a/lib/lp/oci/browser/ocirecipebuild.py
+++ b/lib/lp/oci/browser/ocirecipebuild.py
@@ -4,35 +4,32 @@
"""OCI recipe build views."""
__all__ = [
- 'OCIRecipeBuildCancelView',
- 'OCIRecipeBuildContextMenu',
- 'OCIRecipeBuildNavigation',
- 'OCIRecipeBuildRescoreView',
- 'OCIRecipeBuildView',
- ]
+ "OCIRecipeBuildCancelView",
+ "OCIRecipeBuildContextMenu",
+ "OCIRecipeBuildNavigation",
+ "OCIRecipeBuildRescoreView",
+ "OCIRecipeBuildView",
+]
from zope.interface import Interface
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.oci.interfaces.ocirecipebuild import (
CannotScheduleRegistryUpload,
IOCIRecipeBuild,
- )
+)
from lp.services.librarian.browser import (
FileNavigationMixin,
ProxiedLibraryFileAlias,
- )
+)
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
- canonical_url,
ContextMenu,
- enabled_with_permission,
Link,
Navigation,
- )
+ canonical_url,
+ enabled_with_permission,
+)
from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
@@ -53,20 +50,29 @@ class OCIRecipeBuildContextMenu(ContextMenu):
@enabled_with_permission("launchpad.Edit")
def retry(self):
return Link(
- "+retry", "Retry this build", icon="retry",
- enabled=self.context.can_be_retried)
+ "+retry",
+ "Retry this build",
+ icon="retry",
+ enabled=self.context.can_be_retried,
+ )
@enabled_with_permission("launchpad.Edit")
def cancel(self):
return Link(
- "+cancel", "Cancel build", icon="remove",
- enabled=self.context.can_be_cancelled)
+ "+cancel",
+ "Cancel build",
+ icon="remove",
+ enabled=self.context.can_be_cancelled,
+ )
@enabled_with_permission("launchpad.Admin")
def rescore(self):
return Link(
- "+rescore", "Rescore build", icon="edit",
- enabled=self.context.can_be_rescored)
+ "+rescore",
+ "Rescore build",
+ icon="edit",
+ enabled=self.context.can_be_rescored,
+ )
class OCIRecipeBuildView(LaunchpadFormView):
@@ -89,7 +95,9 @@ class OCIRecipeBuildView(LaunchpadFormView):
return [
ProxiedLibraryFileAlias(alias, self.context)
- for _, alias, _ in self.context.getFiles() if not alias.deleted]
+ for _, alias, _ in self.context.getFiles()
+ if not alias.deleted
+ ]
@cachedproperty
def has_files(self):
@@ -109,7 +117,8 @@ class OCIRecipeBuildView(LaunchpadFormView):
else:
self.request.response.addInfoNotification(
"An upload has been scheduled and will run as soon as "
- "possible.")
+ "possible."
+ )
class OCIRecipeBuildRetryView(LaunchpadFormView):
@@ -123,6 +132,7 @@ class OCIRecipeBuildRetryView(LaunchpadFormView):
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Retry build", name="retry")
@@ -130,7 +140,8 @@ class OCIRecipeBuildRetryView(LaunchpadFormView):
"""Retry the build."""
if not self.context.can_be_retried:
self.request.response.addErrorNotification(
- "Build cannot be retried")
+ "Build cannot be retried"
+ )
else:
self.context.retry()
self.request.response.addInfoNotification("Build has been queued")
@@ -149,6 +160,7 @@ class OCIRecipeBuildCancelView(LaunchpadFormView):
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Cancel build", name="cancel")
@@ -168,12 +180,14 @@ class OCIRecipeBuildRescoreView(LaunchpadFormView):
if self.context.can_be_rescored:
return super().__call__()
self.request.response.addWarningNotification(
- "Cannot rescore this build because it is not queued.")
+ "Cannot rescore this build because it is not queued."
+ )
self.request.response.redirect(canonical_url(self.context))
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Rescore build", name="rescore")
diff --git a/lib/lp/oci/browser/ocirecipesubscription.py b/lib/lp/oci/browser/ocirecipesubscription.py
index c4f016f..65643ba 100644
--- a/lib/lp/oci/browser/ocirecipesubscription.py
+++ b/lib/lp/oci/browser/ocirecipesubscription.py
@@ -3,9 +3,7 @@
"""OCI recipe subscription views."""
-__all__ = [
- 'OCIRecipePortletSubscribersContent'
-]
+__all__ = ["OCIRecipePortletSubscribersContent"]
from zope.component import getUtility
from zope.formlib.form import action
@@ -14,17 +12,14 @@ from zope.security.interfaces import ForbiddenAttribute
from lp.app.browser.launchpadform import (
LaunchpadEditFormView,
LaunchpadFormView,
- )
+)
from lp.oci.interfaces.ocirecipesubscription import IOCIRecipeSubscription
from lp.registry.interfaces.person import IPersonSet
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView, canonical_url
from lp.services.webapp.authorization import (
check_permission,
precache_permission_for_objects,
- )
+)
class OCIRecipePortletSubscribersContent(LaunchpadView):
@@ -38,20 +33,28 @@ class OCIRecipePortletSubscribersContent(LaunchpadView):
# need the expense of running several complex SQL queries.
subscriptions = list(self.context.subscriptions)
person_ids = [sub.person.id for sub in subscriptions]
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- person_ids, need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True
+ )
+ )
if self.user is not None:
subscribers = [
- subscription.person for subscription in subscriptions]
+ subscription.person for subscription in subscriptions
+ ]
precache_permission_for_objects(
- self.request, "launchpad.LimitedView", subscribers)
+ self.request, "launchpad.LimitedView", subscribers
+ )
visible_subscriptions = [
- subscription for subscription in subscriptions
- if check_permission("launchpad.LimitedView", subscription.person)]
+ subscription
+ for subscription in subscriptions
+ if check_permission("launchpad.LimitedView", subscription.person)
+ ]
return sorted(
visible_subscriptions,
- key=lambda subscription: subscription.person.displayname)
+ key=lambda subscription: subscription.person.displayname,
+ )
class RedirectToOCIRecipeMixin:
@@ -73,23 +76,25 @@ class RedirectToOCIRecipeMixin:
cancel_url = next_url
-class OCIRecipeSubscriptionEditView(RedirectToOCIRecipeMixin,
- LaunchpadEditFormView):
+class OCIRecipeSubscriptionEditView(
+ RedirectToOCIRecipeMixin, LaunchpadEditFormView
+):
"""The view for editing OCI recipe subscriptions."""
+
schema = IOCIRecipeSubscription
field_names = []
@property
def page_title(self):
return (
- "Edit subscription to OCI recipe %s" %
- self.ocirecipe.displayname)
+ "Edit subscription to OCI recipe %s" % self.ocirecipe.displayname
+ )
@property
def label(self):
return (
- "Edit subscription to OCI recipe for %s" %
- self.person.displayname)
+ "Edit subscription to OCI recipe for %s" % self.person.displayname
+ )
def initialize(self):
self.ocirecipe = self.context.recipe
@@ -102,11 +107,13 @@ class OCIRecipeSubscriptionEditView(RedirectToOCIRecipeMixin,
self.ocirecipe.unsubscribe(self.person, self.user)
self.request.response.addNotification(
"%s has been unsubscribed from this OCI recipe."
- % self.person.displayname)
+ % self.person.displayname
+ )
-class _OCIRecipeSubscriptionCreationView(RedirectToOCIRecipeMixin,
- LaunchpadFormView):
+class _OCIRecipeSubscriptionCreationView(
+ RedirectToOCIRecipeMixin, LaunchpadFormView
+):
"""Contains the common functionality of the Add and Edit views."""
schema = IOCIRecipeSubscription
@@ -127,11 +134,13 @@ class OCIRecipeSubscriptionAddView(_OCIRecipeSubscriptionCreationView):
# subscribed before continuing.
if self.context.getSubscription(self.user) is not None:
self.request.response.addNotification(
- "You are already subscribed to this OCI recipe.")
+ "You are already subscribed to this OCI recipe."
+ )
else:
self.context.subscribe(self.user, self.user)
self.request.response.addNotification(
- "You have subscribed to this OCI recipe.")
+ "You have subscribed to this OCI recipe."
+ )
class OCIRecipeSubscriptionAddOtherView(_OCIRecipeSubscriptionCreationView):
@@ -150,12 +159,14 @@ class OCIRecipeSubscriptionAddOtherView(_OCIRecipeSubscriptionCreationView):
if "person" in data:
person = data["person"]
subscription = self.context.getSubscription(person)
- if (subscription is None
- and not self.context.userCanBeSubscribed(person)):
+ if subscription is None and not self.context.userCanBeSubscribed(
+ person
+ ):
self.setFieldError(
"person",
"Open and delegated teams cannot be subscribed to "
- "private OCI recipes.")
+ "private OCI recipes.",
+ )
@action("Subscribe", name="subscribe_action")
def subscribe_action(self, action, data):
@@ -165,9 +176,11 @@ class OCIRecipeSubscriptionAddOtherView(_OCIRecipeSubscriptionCreationView):
if subscription is None:
self.context.subscribe(person, self.user)
self.request.response.addNotification(
- "%s has been subscribed to this OCI recipe." %
- person.displayname)
+ "%s has been subscribed to this OCI recipe."
+ % person.displayname
+ )
else:
self.request.response.addNotification(
- "%s was already subscribed to this OCI recipe." %
- person.displayname)
+ "%s was already subscribed to this OCI recipe."
+ % person.displayname
+ )
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index b135ed8..3050f61 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -3,16 +3,13 @@
"""Test OCI recipe views."""
-from datetime import (
- datetime,
- timedelta,
- )
+from datetime import datetime, timedelta
from operator import attrgetter
from urllib.parse import quote
-from fixtures import FakeLogger
import pytz
import soupmatchers
+from fixtures import FakeLogger
from storm.locals import Store
from testtools.matchers import (
Equals,
@@ -21,7 +18,7 @@ from testtools.matchers import (
MatchesSetwise,
MatchesStructure,
Not,
- )
+)
from zope.component import getUtility
from zope.publisher.interfaces import NotFound
from zope.security.interfaces import Unauthorized
@@ -38,20 +35,18 @@ from lp.oci.browser.ocirecipe import (
OCIRecipeAdminView,
OCIRecipeEditView,
OCIRecipeView,
- )
+)
from lp.oci.interfaces.ocipushrule import (
IOCIPushRuleSet,
OCIPushRuleAlreadyExists,
- )
+)
from lp.oci.interfaces.ocirecipe import (
+ OCI_RECIPE_ALLOW_CREATE,
CannotModifyOCIRecipeProcessor,
IOCIRecipeSet,
- OCI_RECIPE_ALLOW_CREATE,
- )
+)
from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
-from lp.oci.interfaces.ociregistrycredentials import (
- IOCIRegistryCredentialsSet,
- )
+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentialsSet
from lp.oci.tests.helpers import OCIConfigHelperMixin
from lp.registry.enums import BranchSharingPolicy
from lp.registry.interfaces.person import IPersonSet
@@ -64,36 +59,27 @@ from lp.services.propertycache import get_property_cache
from lp.services.webapp import canonical_url
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.testing import (
+ BrowserTestCase,
+ TestCaseWithFactory,
admin_logged_in,
anonymous_logged_in,
- BrowserTestCase,
login,
login_person,
person_logged_in,
record_two_runs,
- TestCaseWithFactory,
time_counter,
- )
+)
from lp.testing.dbuser import dbuser
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
-from lp.testing.matchers import (
- MatchesPickerText,
- MatchesTagText,
- )
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
+from lp.testing.matchers import MatchesPickerText, MatchesTagText
from lp.testing.pages import (
extract_text,
find_main_content,
find_tag_by_id,
find_tags_by_class,
- )
+)
from lp.testing.publication import test_traverse
-from lp.testing.views import (
- create_initialized_view,
- create_view,
- )
+from lp.testing.views import create_initialized_view, create_view
class TestOCIRecipeNavigation(TestCaseWithFactory):
@@ -102,19 +88,25 @@ class TestOCIRecipeNavigation(TestCaseWithFactory):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_canonical_url(self):
owner = self.factory.makePerson(name="person")
distribution = self.factory.makeDistribution(name="distro")
oci_project = self.factory.makeOCIProject(
- pillar=distribution, ociprojectname="oci-project")
+ pillar=distribution, ociprojectname="oci-project"
+ )
recipe = self.factory.makeOCIRecipe(
- name="recipe", registrant=owner, owner=owner,
- oci_project=oci_project)
+ name="recipe",
+ registrant=owner,
+ owner=owner,
+ oci_project=oci_project,
+ )
self.assertEqual(
"http://launchpad.test/~person/distro/+oci/oci-project/"
- "+recipe/recipe", canonical_url(recipe))
+ "+recipe/recipe",
+ canonical_url(recipe),
+ )
def test_recipe_traverse_distribution(self):
# Make sure we can reach recipe of distro-based OCI projects.
@@ -122,9 +114,14 @@ class TestOCIRecipeNavigation(TestCaseWithFactory):
oci_project = self.factory.makeOCIProject(pillar=distro)
recipe = self.factory.makeOCIRecipe(oci_project=oci_project)
obj, _, _ = test_traverse(
- "http://launchpad.test/~%s/%s/+oci/%s/+recipe/%s" % (
- recipe.owner.name, recipe.oci_project.pillar.name,
- recipe.oci_project.name, recipe.name))
+ "http://launchpad.test/~%s/%s/+oci/%s/+recipe/%s"
+ % (
+ recipe.owner.name,
+ recipe.oci_project.pillar.name,
+ recipe.oci_project.name,
+ recipe.name,
+ )
+ )
self.assertEqual(recipe, obj)
def test_recipe_traverse_project(self):
@@ -133,9 +130,14 @@ class TestOCIRecipeNavigation(TestCaseWithFactory):
oci_project = self.factory.makeOCIProject(pillar=project)
recipe = self.factory.makeOCIRecipe(oci_project=oci_project)
obj, _, _ = test_traverse(
- "http://launchpad.test/~%s/%s/+oci/%s/+recipe/%s" % (
- recipe.owner.name, recipe.oci_project.pillar.name,
- recipe.oci_project.name, recipe.name))
+ "http://launchpad.test/~%s/%s/+oci/%s/+recipe/%s"
+ % (
+ recipe.owner.name,
+ recipe.oci_project.pillar.name,
+ recipe.oci_project.name,
+ recipe.name,
+ )
+ )
self.assertEqual(recipe, obj)
@@ -147,20 +149,24 @@ class BaseTestOCIRecipeView(BrowserTestCase):
super().setUp()
self.useFixture(FakeLogger())
self.person = self.factory.makePerson(
- name="test-person", displayname="Test Person")
+ name="test-person", displayname="Test Person"
+ )
class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
self.distroseries = self.factory.makeDistroSeries()
self.distribution = self.distroseries.distribution
- self.useFixture(FeatureFixture({
- OCI_RECIPE_ALLOW_CREATE: "on",
- "oci.build_series.%s" % self.distribution.name:
- self.distroseries.name,
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s"
+ % self.distribution.name: self.distroseries.name,
+ }
+ )
+ )
self.setConfig()
def setUpDistroSeries(self):
@@ -169,138 +175,166 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
for name in processor_names:
processor = getUtility(IProcessorSet).getByName(name)
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=name,
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag=name,
+ processor=processor,
+ )
def assertProcessorControls(self, processors_control, enabled, disabled):
matchers = [
MatchesStructure.byEquality(optionValue=name, disabled=False)
- for name in enabled]
- matchers.extend([
- MatchesStructure.byEquality(optionValue=name, disabled=True)
- for name in disabled])
+ for name in enabled
+ ]
+ matchers.extend(
+ [
+ MatchesStructure.byEquality(optionValue=name, disabled=True)
+ for name in disabled
+ ]
+ )
self.assertThat(processors_control.controls, MatchesSetwise(*matchers))
def test_create_new_recipe_not_logged_in(self):
oci_project = self.factory.makeOCIProject()
self.assertRaises(
- Unauthorized, self.getViewBrowser, oci_project,
- view_name="+new-recipe", no_login=True)
+ Unauthorized,
+ self.getViewBrowser,
+ oci_project,
+ view_name="+new-recipe",
+ no_login=True,
+ )
def test_create_new_recipe(self):
oci_project = self.factory.makeOCIProject()
oci_project_display = oci_project.display_name
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
source_display = git_ref.display_name
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
browser.getControl("Create OCI recipe").click()
content = find_main_content(browser.contents)
self.assertEqual("recipe-name", extract_text(content.h1))
self.assertThat(
- "Recipe description",
- MatchesTagText(content, "recipe-description"))
+ "Recipe description", MatchesTagText(content, "recipe-description")
+ )
self.assertThat(
- "Test Person", MatchesPickerText(content, "edit-owner"))
+ "Test Person", MatchesPickerText(content, "edit-owner")
+ )
self.assertThat(
"OCI project:\n%s" % oci_project_display,
- MatchesTagText(content, "oci-project"))
+ MatchesTagText(content, "oci-project"),
+ )
self.assertThat(
"Source:\n%s\nEdit OCI recipe" % source_display,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build file path:\nDockerfile\n"
"Edit OCI recipe\n"
"Build context directory:\n.\n"
"Edit OCI recipe",
- MatchesTagText(content, "build-file"))
+ MatchesTagText(content, "build-file"),
+ )
self.assertThat(
"Build schedule:\nBuilt on request\nEdit OCI recipe\n",
- MatchesTagText(content, "build-schedule"))
+ MatchesTagText(content, "build-schedule"),
+ )
self.assertThat(
- "Official recipe:\nNo",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nNo", MatchesTagText(content, "official-recipe")
+ )
def test_create_new_available_information_types(self):
public_pillar = self.factory.makeProduct(owner=self.person)
private_pillar = self.factory.makeProduct(
owner=self.person,
information_type=InformationType.PROPRIETARY,
- branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY,
+ )
public_oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=public_pillar)
+ registrant=self.person, pillar=public_pillar
+ )
private_oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=private_pillar)
+ registrant=self.person, pillar=private_pillar
+ )
# Public pillar.
browser = self.getViewBrowser(
- public_oci_project, view_name="+new-recipe", user=self.person)
+ public_oci_project, view_name="+new-recipe", user=self.person
+ )
self.assertContentEqual(
- ['PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA'],
- browser.getControl(name="field.information_type").options)
+ ["PUBLIC", "PUBLICSECURITY", "PRIVATESECURITY", "USERDATA"],
+ browser.getControl(name="field.information_type").options,
+ )
# Proprietary pillar.
browser = self.getViewBrowser(
- private_oci_project, view_name="+new-recipe", user=self.person)
+ private_oci_project, view_name="+new-recipe", user=self.person
+ )
self.assertContentEqual(
- ['PROPRIETARY'],
- browser.getControl(name="field.information_type").options)
+ ["PROPRIETARY"],
+ browser.getControl(name="field.information_type").options,
+ )
def test_create_new_recipe_invalid_format(self):
oci_project = self.factory.makeOCIProject()
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/invalid'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/invalid"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
browser.getControl("Create OCI recipe").click()
self.assertIn("Branch does not match format", browser.contents)
def test_create_new_recipe_with_build_args(self):
oci_project = self.factory.makeOCIProject()
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
- browser.getControl("Build-time ARG variables").value = (
- "VAR1=10\nVAR2=20")
+ browser.getControl(
+ "Build-time ARG variables"
+ ).value = "VAR1=10\nVAR2=20"
browser.getControl("Create OCI recipe").click()
content = find_main_content(browser.contents)
self.assertEqual("recipe-name", extract_text(content.h1))
self.assertThat(
"Build-time\nARG variables:\nVAR1=10\nVAR2=20",
- MatchesTagText(content, "build-args"))
+ MatchesTagText(content, "build-args"),
+ )
def test_create_new_recipe_with_image_name(self):
oci_project = self.factory.makeOCIProject()
credentials = self.factory.makeOCIRegistryCredentials()
with person_logged_in(oci_project.distribution.owner):
oci_project.distribution.oci_registry_credentials = credentials
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
image_name = self.factory.getUniqueUnicode()
@@ -309,29 +343,35 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertThat(
"Registry image name:\n{}".format(image_name),
- MatchesTagText(content, "image-name"))
+ MatchesTagText(content, "image-name"),
+ )
def test_create_new_recipe_users_teams_as_owner_options(self):
# Teams that the user is in are options for the OCI recipe owner.
self.factory.makeTeam(
- name="test-team", displayname="Test Team", members=[self.person])
+ name="test-team", displayname="Test Team", members=[self.person]
+ )
oci_project = self.factory.makeOCIProject()
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
options = browser.getControl("Owner").displayOptions
self.assertEqual(
["Test Person (test-person)", "Test Team (test-team)"],
- sorted(str(option) for option in options))
+ sorted(str(option) for option in options),
+ )
def test_create_new_recipe_display_processors(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
processors = browser.getControl(name="field.processors")
self.assertContentEqual(
["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
- [extract_text(option) for option in processors.displayOptions])
+ [extract_text(option) for option in processors.displayOptions],
+ )
self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
@@ -341,35 +381,43 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
proc_armhf = self.factory.makeProcessor(
- name="armhf", restricted=True, build_by_default=False)
+ name="armhf", restricted=True, build_by_default=False
+ )
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag="armhf",
- processor=proc_armhf)
+ distroseries=self.distroseries,
+ architecturetag="armhf",
+ processor=proc_armhf,
+ )
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
processors = browser.getControl(name="field.processors")
self.assertProcessorControls(
- processors, ["386", "amd64", "hppa"], ["armhf"])
+ processors, ["386", "amd64", "hppa"], ["armhf"]
+ )
def test_create_new_recipe_processors(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
processors = browser.getControl(name="field.processors")
processors.value = ["386", "amd64"]
browser.getControl(name="field.name").value = "recipe-name"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
browser.getControl("Create OCI recipe").click()
login_person(self.person)
recipe = getUtility(IOCIRecipeSet).getByName(
- self.person, oci_project, "recipe-name")
+ self.person, oci_project, "recipe-name"
+ )
self.assertContentEqual(
- ["386", "amd64"], [proc.name for proc in recipe.processors])
+ ["386", "amd64"], [proc.name for proc in recipe.processors]
+ )
def test_create_new_recipe_no_default_repo_warning(self):
self.setUpDistroSeries()
@@ -377,12 +425,14 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
with admin_logged_in():
oci_project_url = canonical_url(oci_project)
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
error_message = (
- 'The default git repository for this OCI project was not created '
- 'yet.<br/>'
+ "The default git repository for this OCI project was not created "
+ "yet.<br/>"
'Check the <a href="{url}">OCI project page</a> for instructions '
- 'on how to create one.').format(url=oci_project_url)
+ "on how to create one."
+ ).format(url=oci_project_url)
self.assertIn(error_message, browser.contents)
def test_create_new_recipe_with_default_repo_already_created(self):
@@ -390,52 +440,73 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
repository = self.factory.makeGitRepository(
name=oci_project.name,
- target=oci_project, owner=self.person, registrant=self.person)
+ target=oci_project,
+ owner=self.person,
+ registrant=self.person,
+ )
with person_logged_in(self.distribution.owner):
getUtility(IGitRepositorySet).setDefaultRepository(
- oci_project, repository)
+ oci_project, repository
+ )
default_repo_path = "%s/+oci/%s" % (
- self.distribution.name, oci_project.name)
+ self.distribution.name,
+ oci_project.name,
+ )
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
error_message = (
- 'The default git repository for this OCI project was not created '
- 'yet.')
+ "The default git repository for this OCI project was not created "
+ "yet."
+ )
self.assertNotIn(error_message, browser.contents)
- self.assertThat(browser.contents, soupmatchers.HTMLContains(
- soupmatchers.Tag(
- 'Repository pre-filled', 'input', attrs={
- "id": "field.git_ref.repository",
- "value": default_repo_path})))
+ self.assertThat(
+ browser.contents,
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "Repository pre-filled",
+ "input",
+ attrs={
+ "id": "field.git_ref.repository",
+ "value": default_repo_path,
+ },
+ )
+ ),
+ )
def test_official_is_disabled(self):
oci_project = self.factory.makeOCIProject()
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
official_control = browser.getControl("Official recipe")
self.assertTrue(official_control.disabled)
def test_official_is_enabled(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
official_control = browser.getControl("Official recipe")
self.assertFalse(official_control.disabled)
def test_set_official(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
official_control = browser.getControl("Official recipe")
official_control.selected = True
@@ -443,28 +514,29 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertThat(
- "Official recipe:\nYes",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nYes", MatchesTagText(content, "official-recipe")
+ )
def test_set_official_multiple(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
# do it once
oci_project = self.factory.makeOCIProject(pillar=distribution)
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
# and then do it again
oci_project2 = self.factory.makeOCIProject(pillar=distribution)
- [git_ref2] = self.factory.makeGitRefs(
- paths=['refs/heads/v3.0-20.04'])
+ [git_ref2] = self.factory.makeGitRefs(paths=["refs/heads/v3.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
official_control = browser.getControl("Official recipe")
official_control.selected = True
@@ -472,15 +544,17 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertThat(
- "Official recipe:\nYes",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nYes", MatchesTagText(content, "official-recipe")
+ )
browser2 = self.getViewBrowser(
- oci_project2, view_name="+new-recipe", user=self.person)
+ oci_project2, view_name="+new-recipe", user=self.person
+ )
browser2.getControl(name="field.name").value = "recipe-name"
browser2.getControl("Description").value = "Recipe description"
- browser2.getControl(name="field.git_ref.repository").value = (
- git_ref2.repository.identity)
+ browser2.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref2.repository.identity
browser2.getControl(name="field.git_ref.path").value = git_ref2.path
official_control = browser2.getControl("Official recipe")
official_control.selected = True
@@ -488,28 +562,30 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser2.contents)
self.assertThat(
- "Official recipe:\nYes",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nYes", MatchesTagText(content, "official-recipe")
+ )
browser.reload()
content = find_main_content(browser.contents)
self.assertThat(
- "Official recipe:\nYes",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nYes", MatchesTagText(content, "official-recipe")
+ )
def test_set_official_no_permissions(self):
distro_owner = self.factory.makePerson()
distribution = self.factory.makeDistribution(
- oci_project_admin=distro_owner)
+ oci_project_admin=distro_owner
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = git_ref.path
official_control = browser.getControl("Official recipe")
official_control.selected = True
@@ -517,19 +593,21 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
error_message = (
"You do not have permission to set the official status "
- "of this recipe.")
+ "of this recipe."
+ )
self.assertIn(error_message, browser.contents)
def test_create_recipe_doesnt_override_gitref_errors(self):
oci_project = self.factory.makeOCIProject()
- [git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v2.0-20.04"])
browser = self.getViewBrowser(
- oci_project, view_name="+new-recipe", user=self.person)
+ oci_project, view_name="+new-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "recipe-name"
browser.getControl("Description").value = "Recipe description"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = "non-exist"
browser.getControl("Create OCI recipe").click()
@@ -538,10 +616,9 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_unauthorized(self):
# A non-admin user cannot administer an OCI recipe.
@@ -550,16 +627,21 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
recipe_url = canonical_url(recipe)
browser = self.getViewBrowser(recipe, user=self.person)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Administer OCI recipe")
+ LinkNotFoundError, browser.getLink, "Administer OCI recipe"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, recipe_url + "/+admin",
- user=self.person)
+ Unauthorized,
+ self.getUserBrowser,
+ recipe_url + "/+admin",
+ user=self.person,
+ )
def test_admin_recipe(self):
# Admins can change require_virtualized.
login("admin@xxxxxxxxxxxxx")
commercial_admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin]
+ )
login_person(self.person)
recipe = self.factory.makeOCIRecipe(registrant=self.person)
self.assertTrue(recipe.require_virtualized)
@@ -579,30 +661,36 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
# Administering an OCI recipe sets the date_last_modified property.
login("admin@xxxxxxxxxxxxx")
ppa_admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).ppa_admin])
+ member_of=[getUtility(ILaunchpadCelebrities).ppa_admin]
+ )
login_person(self.person)
date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, date_created=date_created)
+ registrant=self.person, date_created=date_created
+ )
login_person(ppa_admin)
view = OCIRecipeAdminView(recipe, LaunchpadTestRequest())
view.initialize()
view.request_action.success({"require_virtualized": False})
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
self.distroseries = self.factory.makeDistroSeries()
self.distribution = self.distroseries.distribution
- self.useFixture(FeatureFixture({
- OCI_RECIPE_ALLOW_CREATE: "on",
- "oci.build_series.%s" % self.distribution.name:
- self.distroseries.name,
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s"
+ % self.distribution.name: self.distroseries.name,
+ }
+ )
+ )
self.setConfig()
def setUpDistroSeries(self):
@@ -611,34 +699,45 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
for name in processor_names:
processor = getUtility(IProcessorSet).getByName(name)
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=name,
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag=name,
+ processor=processor,
+ )
def assertRecipeProcessors(self, recipe, names):
self.assertContentEqual(
- names, [processor.name for processor in recipe.processors])
+ names, [processor.name for processor in recipe.processors]
+ )
def assertProcessorControls(self, processors_control, enabled, disabled):
matchers = [
MatchesStructure.byEquality(optionValue=name, disabled=False)
- for name in enabled]
- matchers.extend([
- MatchesStructure.byEquality(optionValue=name, disabled=True)
- for name in disabled])
+ for name in enabled
+ ]
+ matchers.extend(
+ [
+ MatchesStructure.byEquality(optionValue=name, disabled=True)
+ for name in disabled
+ ]
+ )
self.assertThat(processors_control.controls, MatchesSetwise(*matchers))
def assertShowsPrivateBanner(self, browser):
banners = find_tags_by_class(
- browser.contents, "private_banner_container")
+ browser.contents, "private_banner_container"
+ )
self.assertEqual(1, len(banners))
self.assertEqual(
- 'The information on this page is private.',
- extract_text(banners[0]))
+ "The information on this page is private.",
+ extract_text(banners[0]),
+ )
def test_edit_private_recipe_shows_banner(self):
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- information_type=InformationType.USERDATA)
+ registrant=self.person,
+ owner=self.person,
+ information_type=InformationType.USERDATA,
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
self.assertShowsPrivateBanner(browser)
@@ -647,14 +746,20 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
oci_project = self.factory.makeOCIProject()
oci_project_display = oci_project.display_name
[old_git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v1.0-20.04'])
+ paths=["refs/heads/v1.0-20.04"]
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, git_ref=old_git_ref)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=old_git_ref,
+ )
self.factory.makeTeam(
- name="new-team", displayname="New Team", members=[self.person])
+ name="new-team", displayname="New Team", members=[self.person]
+ )
[new_git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v2.0-20.04'])
+ paths=["refs/heads/v2.0-20.04"]
+ )
self.factory.makeOCIPushRule(recipe=recipe)
browser = self.getViewBrowser(recipe, user=self.person)
@@ -662,8 +767,9 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
browser.getControl("Owner").value = ["new-team"]
browser.getControl(name="field.name").value = "new-name"
browser.getControl("Description").value = "New description"
- browser.getControl(name="field.git_ref.repository").value = (
- new_git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = new_git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = new_git_ref.path
browser.getControl("Build file path").value = "Dockerfile-2"
browser.getControl("Build directory context").value = "apath"
@@ -675,32 +781,42 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
self.assertThat(
"OCI project:\n%s" % oci_project_display,
- MatchesTagText(content, "oci-project"))
+ MatchesTagText(content, "oci-project"),
+ )
self.assertThat(
"Source:\n%s\nEdit OCI recipe" % new_git_ref.display_name,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build file path:\nDockerfile-2\n"
"Edit OCI recipe\n"
"Build context directory:\napath\n"
"Edit OCI recipe",
- MatchesTagText(content, "build-file"))
+ MatchesTagText(content, "build-file"),
+ )
self.assertThat(
"Build schedule:\nBuilt daily\nEdit OCI recipe\n",
- MatchesTagText(content, "build-schedule"))
+ MatchesTagText(content, "build-schedule"),
+ )
def test_edit_recipe_invalid_branch(self):
oci_project = self.factory.makeOCIProject()
repository = self.factory.makeGitRepository()
[old_git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v1.0-20.04'], repository=repository)
+ paths=["refs/heads/v1.0-20.04"], repository=repository
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, git_ref=old_git_ref)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=old_git_ref,
+ )
self.factory.makeTeam(
- name="new-team", displayname="New Team", members=[self.person])
+ name="new-team", displayname="New Team", members=[self.person]
+ )
[new_git_ref] = self.factory.makeGitRefs(
- repository=repository, paths=['refs/heads/invalid'])
+ repository=repository, paths=["refs/heads/invalid"]
+ )
self.factory.makeOCIPushRule(recipe=recipe)
browser = self.getViewBrowser(recipe, user=self.person)
@@ -713,25 +829,36 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
pillar = self.factory.makeProduct(
owner=self.person,
information_type=InformationType.PUBLIC,
- branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
+ branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY,
+ )
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=pillar)
+ registrant=self.person, pillar=pillar
+ )
[git_ref] = self.factory.makeGitRefs(
- owner=self.person,
- paths=['refs/heads/v2.0-20.04'])
+ owner=self.person, paths=["refs/heads/v2.0-20.04"]
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, git_ref=git_ref,
- information_type=InformationType.PUBLIC)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=git_ref,
+ information_type=InformationType.PUBLIC,
+ )
- browser = self.getViewBrowser(recipe, '+edit', user=self.person)
+ browser = self.getViewBrowser(recipe, "+edit", user=self.person)
# Make sure we are showing all available information types:
info_type_field = browser.getControl(name="field.information_type")
- self.assertContentEqual([
- 'PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA',
- 'PROPRIETARY'],
- info_type_field.options)
+ self.assertContentEqual(
+ [
+ "PUBLIC",
+ "PUBLICSECURITY",
+ "PRIVATESECURITY",
+ "USERDATA",
+ "PROPRIETARY",
+ ],
+ info_type_field.options,
+ )
info_type_field.value = InformationType.PROPRIETARY.name
browser.getControl("Update OCI recipe").click()
@@ -739,39 +866,51 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_edit_recipe_on_public_pillar_information_types(self):
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person)
- browser = self.getViewBrowser(recipe, '+edit', user=self.person)
+ registrant=self.person, owner=self.person
+ )
+ browser = self.getViewBrowser(recipe, "+edit", user=self.person)
info_type_field = browser.getControl(name="field.information_type")
self.assertContentEqual(
- ['PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA'],
- info_type_field.options)
+ ["PUBLIC", "PUBLICSECURITY", "PRIVATESECURITY", "USERDATA"],
+ info_type_field.options,
+ )
def test_edit_recipe_sets_date_last_modified(self):
# Editing an OCI recipe sets the date_last_modified property.
date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, date_created=date_created)
+ registrant=self.person, date_created=date_created
+ )
with person_logged_in(self.person):
view = OCIRecipeEditView(recipe, LaunchpadTestRequest())
view.initialize()
- view.request_action.success({
- "owner": recipe.owner,
- "name": "changed",
- "description": "changed",
- })
+ view.request_action.success(
+ {
+ "owner": recipe.owner,
+ "name": "changed",
+ "description": "changed",
+ }
+ )
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
def test_edit_recipe_already_exists(self):
oci_project = self.factory.makeOCIProject()
oci_project_display = oci_project.display_name
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, name="one")
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ name="one",
+ )
self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, name="two")
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ name="two",
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
browser.getControl(name="field.name").value = "two"
@@ -779,29 +918,35 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
self.assertEqual(
"There is already an OCI recipe owned by Test Person in %s with "
"this name." % oci_project_display,
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
def test_display_processors(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
processors = browser.getControl(name="field.processors")
self.assertContentEqual(
["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
- [extract_text(option) for option in processors.displayOptions])
+ [extract_text(option) for option in processors.displayOptions],
+ )
self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
def test_edit_processors(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
self.assertRecipeProcessors(recipe, ["386", "amd64", "hppa"])
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
processors = browser.getControl(name="field.processors")
self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
processors.value = ["386", "amd64"]
@@ -813,10 +958,14 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"})
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ build_args={"VAR1": "xxx", "VAR2": "uu"},
+ )
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
args = browser.getControl(name="field.build_args")
self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value)
args.value = "VAR=aa\nANOTHER_VAR=bbb"
@@ -824,16 +973,21 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
login_person(self.person)
IStore(recipe).reload(recipe)
self.assertEqual(
- {"VAR": "aa", "ANOTHER_VAR": "bbb"}, recipe.build_args)
+ {"VAR": "aa", "ANOTHER_VAR": "bbb"}, recipe.build_args
+ )
def test_edit_build_args_invalid_content(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"})
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ build_args={"VAR1": "xxx", "VAR2": "uu"},
+ )
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
args = browser.getControl(name="field.build_args")
self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value)
args.value = "VAR=aa\nmessed up text"
@@ -843,7 +997,8 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertIn(
"'messed up text' at line 2 is not a valid KEY=value pair.",
- extract_text(content))
+ extract_text(content),
+ )
# Assert that recipe still have the original build_args.
login_person(self.person)
@@ -859,11 +1014,14 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
name=original_name,
- registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ )
oci_project.setOfficialRecipeStatus(recipe, True)
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
image_name = self.factory.getUniqueUnicode()
field = browser.getControl(name="field.image_name")
# Default is the recipe name
@@ -873,7 +1031,8 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertThat(
"Registry image name:\n{}".format(image_name),
- MatchesTagText(content, "image-name"))
+ MatchesTagText(content, "image-name"),
+ )
def test_edit_with_invisible_processor(self):
# It's possible for existing recipes to have an enabled processor
@@ -885,14 +1044,17 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
proc_386 = getUtility(IProcessorSet).getByName("386")
proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
proc_armel = self.factory.makeProcessor(
- name="armel", restricted=True, build_by_default=False)
+ name="armel", restricted=True, build_by_default=False
+ )
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
recipe.setProcessors([proc_386, proc_amd64, proc_armel])
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
processors = browser.getControl(name="field.processors")
self.assertContentEqual(["386", "amd64"], processors.value)
processors.value = ["amd64"]
@@ -905,20 +1067,26 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
# checkbox in the UI, and the processor cannot be enabled.
self.setUpDistroSeries()
proc_armhf = self.factory.makeProcessor(
- name="armhf", restricted=True, build_by_default=False)
+ name="armhf", restricted=True, build_by_default=False
+ )
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag="armhf",
- processor=proc_armhf)
+ distroseries=self.distroseries,
+ architecturetag="armhf",
+ processor=proc_armhf,
+ )
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
self.assertRecipeProcessors(recipe, ["386", "amd64", "hppa"])
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=recipe.owner)
+ recipe, view_name="+edit", user=recipe.owner
+ )
processors = browser.getControl(name="field.processors")
self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
self.assertProcessorControls(
- processors, ["386", "amd64", "hppa"], ["armhf"])
+ processors, ["386", "amd64", "hppa"], ["armhf"]
+ )
# Even if the user works around the disabled checkbox and forcibly
# enables it, they can't enable the restricted processor.
for control in processors.controls:
@@ -927,7 +1095,8 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
processors.value = ["386", "amd64", "armhf"]
self.assertRaises(
CannotModifyOCIRecipeProcessor,
- browser.getControl("Update OCI recipe").click)
+ browser.getControl("Update OCI recipe").click,
+ )
def test_edit_processors_restricted_already_enabled(self):
# A restricted processor that is already enabled is shown with a
@@ -938,23 +1107,29 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
proc_386 = getUtility(IProcessorSet).getByName("386")
proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
proc_armhf = self.factory.makeProcessor(
- name="armhf", restricted=True, build_by_default=False)
+ name="armhf", restricted=True, build_by_default=False
+ )
self.setUpDistroSeries()
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag="armhf",
- processor=proc_armhf)
+ distroseries=self.distroseries,
+ architecturetag="armhf",
+ processor=proc_armhf,
+ )
oci_project = self.factory.makeOCIProject(pillar=self.distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
recipe.setProcessors([proc_386, proc_amd64, proc_armhf])
self.assertRecipeProcessors(recipe, ["386", "amd64", "armhf"])
browser = self.getUserBrowser(
- canonical_url(recipe) + "/+edit", user=recipe.owner)
+ canonical_url(recipe) + "/+edit", user=recipe.owner
+ )
processors = browser.getControl(name="field.processors")
# armhf is checked but disabled.
self.assertContentEqual(["386", "amd64", "armhf"], processors.value)
self.assertProcessorControls(
- processors, ["386", "amd64", "hppa"], ["armhf"])
+ processors, ["386", "amd64", "hppa"], ["armhf"]
+ )
processors.value = ["386"]
browser.getControl("Update OCI recipe").click()
login_person(self.person)
@@ -963,93 +1138,126 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_edit_without_default_repo_for_ociproject(self):
self.setUpDistroSeries()
repo = self.factory.makeGitRepository(
- owner=self.person, registrant=self.person)
+ owner=self.person, registrant=self.person
+ )
[git_ref] = self.factory.makeGitRefs(
- repository=repo, paths=['refs/heads/v1.0-20.04'])
+ repository=repo, paths=["refs/heads/v1.0-20.04"]
+ )
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=self.distribution)
+ registrant=self.person, pillar=self.distribution
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, oci_project=oci_project, git_ref=git_ref)
+ registrant=self.person, oci_project=oci_project, git_ref=git_ref
+ )
with person_logged_in(self.person):
oci_project_url = canonical_url(oci_project)
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=self.person)
+ recipe, view_name="+edit", user=self.person
+ )
error_message = (
"This recipe's git repository is not in the correct "
'namespace.<br/>Check the <a href="{url}">OCI project page</a> '
- "for instructions on how to create it correctly.")
+ "for instructions on how to create it correctly."
+ )
self.assertIn(
- error_message.format(url=oci_project_url), browser.contents)
+ error_message.format(url=oci_project_url), browser.contents
+ )
def test_edit_repository_is_not_default_for_ociproject(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=self.distribution)
+ registrant=self.person, pillar=self.distribution
+ )
[random_git_ref] = self.factory.makeGitRefs(
- paths=['refs/heads/v1.0-20.04'])
+ paths=["refs/heads/v1.0-20.04"]
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project,
- git_ref=random_git_ref)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=random_git_ref,
+ )
# Make the default git repository that should have been used by the
# recipe.
default_repo = self.factory.makeGitRepository(
name=oci_project.name,
- target=oci_project, owner=self.person, registrant=self.person)
+ target=oci_project,
+ owner=self.person,
+ registrant=self.person,
+ )
with person_logged_in(self.distribution.owner):
getUtility(IGitRepositorySet).setDefaultRepository(
- oci_project, default_repo)
+ oci_project, default_repo
+ )
with person_logged_in(self.person):
- repo_link = GitRepositoryFormatterAPI(default_repo).link('')
+ repo_link = GitRepositoryFormatterAPI(default_repo).link("")
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=self.person)
+ recipe, view_name="+edit", user=self.person
+ )
error_message = (
"This recipe's git repository is not in the correct "
- "namespace.<br/>Consider using {repo} instead.")
- self.assertIn(
- error_message.format(repo=repo_link), browser.contents)
+ "namespace.<br/>Consider using {repo} instead."
+ )
+ self.assertIn(error_message.format(repo=repo_link), browser.contents)
def test_edit_repository_in_the_correct_namespace(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=self.distribution)
+ registrant=self.person, pillar=self.distribution
+ )
default_repo = self.factory.makeGitRepository(
name=oci_project.name,
- target=oci_project, owner=self.person, registrant=self.person)
+ target=oci_project,
+ owner=self.person,
+ registrant=self.person,
+ )
[git_ref] = self.factory.makeGitRefs(
- repository=default_repo, paths=['refs/heads/v1.0-20.04'])
+ repository=default_repo, paths=["refs/heads/v1.0-20.04"]
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project,
- git_ref=git_ref)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=git_ref,
+ )
with person_logged_in(self.person):
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=self.person)
+ recipe, view_name="+edit", user=self.person
+ )
self.assertNotIn(
"This recipe's git repository is not in the correct namespace",
- browser.contents)
+ browser.contents,
+ )
def test_edit_repository_dont_override_important_msgs(self):
self.setUpDistroSeries()
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=self.distribution)
+ registrant=self.person, pillar=self.distribution
+ )
- [git_ref] = self.factory.makeGitRefs(paths=['refs/heads/v1.0-20.04'])
+ [git_ref] = self.factory.makeGitRefs(paths=["refs/heads/v1.0-20.04"])
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, oci_project=oci_project,
- git_ref=git_ref)
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ git_ref=git_ref,
+ )
wrong_namespace_msg = (
- "This recipe's git repository is not in the correct namespace")
+ "This recipe's git repository is not in the correct namespace"
+ )
wrong_ref_path_msg = (
- "The repository at %s does not contain a branch named "
- "'non-existing git-ref'."
- ) % git_ref.repository.display_name
+ "The repository at %s does not contain a branch named "
+ "'non-existing git-ref'."
+ ) % git_ref.repository.display_name
with person_logged_in(self.person):
browser = self.getViewBrowser(
- recipe, view_name="+edit", user=self.person)
+ recipe, view_name="+edit", user=self.person
+ )
self.assertIn(wrong_namespace_msg, browser.contents)
args = browser.getControl(name="field.git_ref.path")
args.value = "non-existing git-ref"
@@ -1062,8 +1270,8 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_official_is_disabled(self):
oci_project = self.factory.makeOCIProject()
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
@@ -1072,12 +1280,13 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_official_is_set_while_disabled(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
non_admin = self.factory.makePerson()
oci_project = self.factory.makeOCIProject(pillar=distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=non_admin, owner=non_admin,
- oci_project=oci_project)
+ registrant=non_admin, owner=non_admin, oci_project=oci_project
+ )
with person_logged_in(self.person):
oci_project.setOfficialRecipeStatus(recipe, True)
browser = self.getViewBrowser(recipe, user=non_admin)
@@ -1088,11 +1297,12 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_official_is_enabled(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
@@ -1101,11 +1311,12 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def test_set_official(self):
distribution = self.factory.makeDistribution(
- oci_project_admin=self.person)
+ oci_project_admin=self.person
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
@@ -1115,17 +1326,18 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
content = find_main_content(browser.contents)
self.assertThat(
- "Official recipe:\nYes",
- MatchesTagText(content, "official-recipe"))
+ "Official recipe:\nYes", MatchesTagText(content, "official-recipe")
+ )
def test_set_official_no_permissions(self):
distro_owner = self.factory.makePerson()
distribution = self.factory.makeDistribution(
- oci_project_admin=distro_owner)
+ oci_project_admin=distro_owner
+ )
oci_project = self.factory.makeOCIProject(pillar=distribution)
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ registrant=self.person, owner=self.person, oci_project=oci_project
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit OCI recipe").click()
@@ -1135,33 +1347,39 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
error_message = (
"You do not have permission to change the official status "
- "of this recipe.")
+ "of this recipe."
+ )
self.assertIn(error_message, browser.contents)
class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_unauthorized(self):
# A user without edit access cannot delete an OCI recipe.
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
recipe_url = canonical_url(recipe)
other_person = self.factory.makePerson()
browser = self.getViewBrowser(recipe, user=other_person)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Delete OCI recipe")
+ LinkNotFoundError, browser.getLink, "Delete OCI recipe"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, recipe_url + "/+delete",
- user=other_person)
+ Unauthorized,
+ self.getUserBrowser,
+ recipe_url + "/+delete",
+ user=other_person,
+ )
def test_delete_recipe_without_builds(self):
# An OCI recipe without builds can be deleted.
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
recipe_url = canonical_url(recipe)
oci_project_url = canonical_url(recipe.oci_project)
browser = self.getViewBrowser(recipe, user=self.person)
@@ -1173,7 +1391,8 @@ class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):
def test_delete_recipe_with_builds(self):
# An OCI recipe with builds can be deleted.
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
ocibuild = self.factory.makeOCIRecipeBuild(recipe=recipe)
job = self.factory.makeOCIRecipeBuildJob(build=ocibuild)
ocifile = self.factory.makeOCIFile(build=ocibuild)
@@ -1191,41 +1410,48 @@ class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):
self.assertRaises(NotFound, browser.open, recipe_url)
# Checks that only the related artifacts were deleted too.
- def obj_exists(obj, search_key='id'):
+ def obj_exists(obj, search_key="id"):
obj = removeSecurityProxy(obj)
store = IStore(obj)
cls = obj.__class__
cls_attribute = getattr(cls, search_key)
identifier = getattr(obj, search_key)
return not store.find(cls, cls_attribute == identifier).is_empty()
+
self.assertFalse(obj_exists(ocibuild))
self.assertFalse(obj_exists(ocifile))
- self.assertFalse(obj_exists(job, 'job_id'))
+ self.assertFalse(obj_exists(job, "job_id"))
self.assertTrue(obj_exists(unrelated_build))
self.assertTrue(obj_exists(unrelated_file))
- self.assertTrue(obj_exists(unrelated_job, 'job_id'))
+ self.assertTrue(obj_exists(unrelated_job, "job_id"))
class TestOCIRecipeView(BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
self.distroseries = self.factory.makeDistroSeries()
processor = getUtility(IProcessorSet).getByName("386")
self.distroarchseries = self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag="i386",
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag="i386",
+ processor=processor,
+ )
self.factory.makeBuilder(virtualized=True)
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def makeOCIRecipe(self, oci_project=None, **kwargs):
if oci_project is None:
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
return self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, name="recipe-name",
- oci_project=oci_project, **kwargs)
+ registrant=self.person,
+ owner=self.person,
+ name="recipe-name",
+ oci_project=oci_project,
+ **kwargs,
+ )
def makeBuild(self, recipe=None, date_created=None, **kwargs):
if recipe is None:
@@ -1233,13 +1459,17 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
if date_created is None:
date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
return self.factory.makeOCIRecipeBuild(
- requester=self.person, recipe=recipe,
+ requester=self.person,
+ recipe=recipe,
distro_arch_series=self.distroarchseries,
- date_created=date_created, **kwargs)
+ date_created=date_created,
+ **kwargs,
+ )
def test_breadcrumb_and_top_header(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
oci_project_name = oci_project.name
oci_project_url = canonical_url(oci_project)
pillar_name = oci_project.pillar.name
@@ -1251,7 +1481,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
view.initialize()
content = view()
breadcrumbs = soupmatchers.Tag(
- "breadcrumbs", "ol", attrs={"class": "breadcrumbs"})
+ "breadcrumbs", "ol", attrs={"class": "breadcrumbs"}
+ )
# Should not have a breadcrumbs (OCI project link should be at the
# top of the page, close to project/distribution name).
@@ -1259,27 +1490,50 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
# OCI project should appear at the top header, right after pillar link.
header = soupmatchers.Tag(
- "subtitle", "h2", attrs={"id": "watermark-heading"})
- self.assertThat(content, soupmatchers.HTMLContains(soupmatchers.Within(
- header, soupmatchers.Tag(
- "pillar link", "a",
- text=pillar_name.title(), attrs={"href": pillar_url}))))
- self.assertThat(content, soupmatchers.HTMLContains(soupmatchers.Within(
- header, soupmatchers.Tag(
- "OCI project link", "a",
- text="%s OCI project" % oci_project_name,
- attrs={"href": oci_project_url}))))
+ "subtitle", "h2", attrs={"id": "watermark-heading"}
+ )
+ self.assertThat(
+ content,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ header,
+ soupmatchers.Tag(
+ "pillar link",
+ "a",
+ text=pillar_name.title(),
+ attrs={"href": pillar_url},
+ ),
+ )
+ ),
+ )
+ self.assertThat(
+ content,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ header,
+ soupmatchers.Tag(
+ "OCI project link",
+ "a",
+ text="%s OCI project" % oci_project_name,
+ attrs={"href": oci_project_url},
+ ),
+ )
+ ),
+ )
def makeRecipe(self, processor_names, **kwargs):
recipe = self.factory.makeOCIRecipe(**kwargs)
processors_list = []
distroseries = self.factory.makeDistroSeries(
- distribution=recipe.oci_project.distribution)
+ distribution=recipe.oci_project.distribution
+ )
for proc_name in processor_names:
proc = getUtility(IProcessorSet).getByName(proc_name)
distro = self.factory.makeDistroArchSeries(
- distroseries=distroseries, architecturetag=proc_name,
- processor=proc)
+ distroseries=distroseries,
+ architecturetag=proc_name,
+ processor=proc,
+ )
distro.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
processors_list.append(proc)
recipe.setProcessors(processors_list)
@@ -1287,15 +1541,23 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
def test_index(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
recipe = self.makeRecipe(
processor_names=["amd64", "386"],
- build_file="Dockerfile", git_ref=ref,
- oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_file="Dockerfile",
+ git_ref=ref,
+ oci_project=oci_project,
+ registrant=self.person,
+ owner=self.person,
+ )
build_request = recipe.requestBuilds(self.person)
builds = recipe.requestBuildsFromJob(self.person, build_request)
job = removeSecurityProxy(build_request).job
@@ -1303,20 +1565,27 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
for build in builds:
removeSecurityProxy(build).updateStatus(
- BuildStatus.BUILDING, builder=None,
- date_started=build.date_created)
+ BuildStatus.BUILDING,
+ builder=None,
+ date_started=build.date_created,
+ )
removeSecurityProxy(build).updateStatus(
- BuildStatus.FULLYBUILT, builder=None,
- date_finished=build.date_started + timedelta(minutes=30))
+ BuildStatus.FULLYBUILT,
+ builder=None,
+ date_finished=build.date_started + timedelta(minutes=30),
+ )
# We also need to account for builds that don't have a build_request
build = self.makeBuild(
- recipe=recipe, status=BuildStatus.FULLYBUILT,
- duration=timedelta(minutes=30))
+ recipe=recipe,
+ status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30),
+ )
browser = self.getViewBrowser(build_request.recipe)
login_person(self.person)
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
.*
OCI recipe information
Owner: Test Person
@@ -1354,26 +1623,37 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
30 minutes ago
Recipe push rules
This OCI recipe has no push rules defined yet.
- """ % (oci_project_display, recipe.build_path),
- extract_text(find_main_content(browser.contents)))
+ """
+ % (oci_project_display, recipe.build_path),
+ extract_text(find_main_content(browser.contents)),
+ )
# Check portlet on side menu.
privacy_tag = find_tag_by_id(browser.contents, "privacy")
self.assertTextMatchesExpressionIgnoreWhitespace(
"This OCI recipe contains Public information",
- extract_text(privacy_tag))
+ extract_text(privacy_tag),
+ )
def test_index_cancelled_build(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
recipe = self.makeRecipe(
processor_names=["amd64", "386"],
- build_file="Dockerfile", git_ref=ref,
- oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_file="Dockerfile",
+ git_ref=ref,
+ oci_project=oci_project,
+ registrant=self.person,
+ owner=self.person,
+ )
build_request = recipe.requestBuilds(self.person)
builds = recipe.requestBuildsFromJob(self.person, build_request)
job = removeSecurityProxy(build_request).job
@@ -1381,20 +1661,27 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
for build in builds:
removeSecurityProxy(build).updateStatus(
- BuildStatus.BUILDING, builder=None,
- date_started=build.date_created)
+ BuildStatus.BUILDING,
+ builder=None,
+ date_started=build.date_created,
+ )
removeSecurityProxy(build).updateStatus(
- BuildStatus.CANCELLED, builder=None,
- date_finished=build.date_started + timedelta(minutes=30))
+ BuildStatus.CANCELLED,
+ builder=None,
+ date_finished=build.date_started + timedelta(minutes=30),
+ )
# We also need to account for builds that don't have a build_request
build = self.makeBuild(
- recipe=recipe, status=BuildStatus.FULLYBUILT,
- duration=timedelta(minutes=30))
+ recipe=recipe,
+ status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30),
+ )
browser = self.getViewBrowser(build_request.recipe)
login_person(self.person)
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
.*
OCI recipe information
Owner: Test Person
@@ -1432,25 +1719,36 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
30 minutes ago
Recipe push rules
This OCI recipe has no push rules defined yet.
- """ % (oci_project_display, recipe.build_path),
- extract_text(find_main_content(browser.contents)))
+ """
+ % (oci_project_display, recipe.build_path),
+ extract_text(find_main_content(browser.contents)),
+ )
# Check portlet on side menu.
privacy_tag = find_tag_by_id(browser.contents, "privacy")
self.assertTextMatchesExpressionIgnoreWhitespace(
"This OCI recipe contains Public information",
- extract_text(privacy_tag))
+ extract_text(privacy_tag),
+ )
def test_index_cancelling_build(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
recipe = self.makeRecipe(
processor_names=["amd64", "386"],
- build_file="Dockerfile", git_ref=ref,
- oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_file="Dockerfile",
+ git_ref=ref,
+ oci_project=oci_project,
+ registrant=self.person,
+ owner=self.person,
+ )
build_request = recipe.requestBuilds(self.person)
builds = recipe.requestBuildsFromJob(self.person, build_request)
job = removeSecurityProxy(build_request).job
@@ -1458,15 +1756,20 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
for build in builds:
removeSecurityProxy(build).updateStatus(
- BuildStatus.BUILDING, builder=None,
- date_started=build.date_created)
+ BuildStatus.BUILDING,
+ builder=None,
+ date_started=build.date_created,
+ )
removeSecurityProxy(build).updateStatus(
- BuildStatus.CANCELLING, builder=None,
- date_finished=build.date_started + timedelta(minutes=30))
+ BuildStatus.CANCELLING,
+ builder=None,
+ date_finished=build.date_started + timedelta(minutes=30),
+ )
browser = self.getViewBrowser(build_request.recipe)
login_person(self.person)
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
.*
There were build failures.
No registry upload requested.
@@ -1483,49 +1786,66 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
\\(estimated\\)
.*
""",
- extract_text(find_main_content(browser.contents)))
+ extract_text(find_main_content(browser.contents)),
+ )
# Check portlet on side menu.
privacy_tag = find_tag_by_id(browser.contents, "privacy")
self.assertTextMatchesExpressionIgnoreWhitespace(
"This OCI recipe contains Public information",
- extract_text(privacy_tag))
+ extract_text(privacy_tag),
+ )
def test_index_for_private_recipe_shows_banner(self):
recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- information_type=InformationType.USERDATA)
+ registrant=self.person,
+ owner=self.person,
+ information_type=InformationType.USERDATA,
+ )
browser = self.getViewBrowser(recipe, user=self.person)
# Check top banner.
banners = find_tags_by_class(
- browser.contents, "private_banner_container")
+ browser.contents, "private_banner_container"
+ )
self.assertEqual(1, len(banners))
self.assertTextMatchesExpressionIgnoreWhitespace(
- 'The information on this page is private.',
- extract_text(banners[0]))
+ "The information on this page is private.",
+ extract_text(banners[0]),
+ )
# Check portlet on side menu.
privacy_tag = find_tag_by_id(browser.contents, "privacy")
self.assertTextMatchesExpressionIgnoreWhitespace(
"This OCI recipe contains Private information",
- extract_text(privacy_tag))
+ extract_text(privacy_tag),
+ )
def test_index_with_build_args(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
recipe = self.makeOCIRecipe(
- oci_project=oci_project, git_ref=ref, build_file="Dockerfile",
- build_args={"VAR1": "123", "VAR2": "XXX"})
+ oci_project=oci_project,
+ git_ref=ref,
+ build_file="Dockerfile",
+ build_args={"VAR1": "123", "VAR2": "XXX"},
+ )
build_path = recipe.build_path
build = self.makeBuild(
- recipe=recipe, status=BuildStatus.FULLYBUILT,
- duration=timedelta(minutes=30))
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ recipe=recipe,
+ status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30),
+ )
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
recipe-name
.*
OCI recipe information
@@ -1551,25 +1871,36 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Successfully built
386
30 minutes ago
- """ % (oci_project_display, build_path),
- self.getMainText(build.recipe))
+ """
+ % (oci_project_display, build_path),
+ self.getMainText(build.recipe),
+ )
def test_index_for_subscriber_without_git_repo_access(self):
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
paths=["refs/heads/v1.0-20.04"],
- information_type=InformationType.PRIVATESECURITY)
+ information_type=InformationType.PRIVATESECURITY,
+ )
recipe = self.makeOCIRecipe(
- oci_project=oci_project, git_ref=ref, build_file="Dockerfile",
- information_type=InformationType.PRIVATESECURITY)
+ oci_project=oci_project,
+ git_ref=ref,
+ build_file="Dockerfile",
+ information_type=InformationType.PRIVATESECURITY,
+ )
with admin_logged_in():
build_path = recipe.build_path
self.makeBuild(
- recipe=recipe, status=BuildStatus.FULLYBUILT,
- duration=timedelta(minutes=30))
+ recipe=recipe,
+ status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30),
+ )
# Subscribe a user.
subscriber = self.factory.makePerson()
@@ -1578,7 +1909,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
with person_logged_in(subscriber):
main_text = self.getMainText(recipe, user=subscriber)
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
recipe-name
.*
OCI recipe information
@@ -1603,15 +1935,19 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Successfully built
386
30 minutes ago
- """ % (oci_project_display, build_path),
- main_text)
+ """
+ % (oci_project_display, build_path),
+ main_text,
+ )
def test_index_success_with_buildlog(self):
# The build log is shown if it is there.
build = self.makeBuild(
- status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
+ status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30)
+ )
build.setLog(self.factory.makeLibraryFileAlias())
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Latest builds
Build status
Upload status
@@ -1627,31 +1963,42 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Successfully built
386
30 minutes ago
- """, self.getMainText(build.recipe))
+ """,
+ self.getMainText(build.recipe),
+ )
def test_index_no_builds(self):
# A message is shown when there are no builds.
recipe = self.factory.makeOCIRecipe()
self.assertIn(
- "This OCI recipe has not been built yet.",
- self.getMainText(recipe))
+ "This OCI recipe has not been built yet.", self.getMainText(recipe)
+ )
def test_index_pending_build(self):
# A pending build is listed as such.
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution)
+ pillar=self.distroseries.distribution
+ )
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
recipe = self.makeRecipe(
processor_names=["amd64", "386"],
- build_file="Dockerfile", git_ref=ref,
- oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_file="Dockerfile",
+ git_ref=ref,
+ oci_project=oci_project,
+ registrant=self.person,
+ owner=self.person,
+ )
build_request = recipe.requestBuilds(self.person)
builds = recipe.requestBuildsFromJob(self.person, build_request)
job = removeSecurityProxy(build_request).job
removeSecurityProxy(job).builds = builds
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Latest builds
Build status
Upload status
@@ -1661,16 +2008,20 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Waiting for builds to start.
a moment ago
in .* \(estimated\)
- """, self.getMainText(recipe))
+ """,
+ self.getMainText(recipe),
+ )
def test_index_request_builds_link(self):
# Recipe owners get a link to allow requesting builds.
owner = self.factory.makePerson()
distroseries = self.factory.makeDistroSeries()
oci_project = self.factory.makeOCIProject(
- pillar=distroseries.distribution)
+ pillar=distroseries.distribution
+ )
recipe = self.factory.makeOCIRecipe(
- registrant=owner, owner=owner, oci_project=oci_project)
+ registrant=owner, owner=owner, oci_project=oci_project
+ )
recipe_name = recipe.name
browser = self.getViewBrowser(recipe, user=owner)
browser.getLink("Request builds").click()
@@ -1681,30 +2032,38 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
# requesting builds.
distroseries = self.factory.makeDistroSeries()
oci_project = self.factory.makeOCIProject(
- pillar=distroseries.distribution)
+ pillar=distroseries.distribution
+ )
recipe = self.factory.makeOCIRecipe(oci_project=oci_project)
recipe_url = canonical_url(recipe)
browser = self.getViewBrowser(recipe, user=self.person)
self.assertRaises(LinkNotFoundError, browser.getLink, "Request builds")
self.assertRaises(
- Unauthorized, self.getUserBrowser, recipe_url + "/+request-builds",
- user=self.person)
+ Unauthorized,
+ self.getUserBrowser,
+ recipe_url + "/+request-builds",
+ user=self.person,
+ )
def setStatus(self, build, status):
build.updateStatus(
- BuildStatus.BUILDING, date_started=build.date_created)
+ BuildStatus.BUILDING, date_started=build.date_created
+ )
build.updateStatus(
- status, date_finished=build.date_started + timedelta(minutes=30))
+ status, date_finished=build.date_started + timedelta(minutes=30)
+ )
def test_builds(self):
# OCIRecipeView.builds produces reasonable results.
recipe = self.makeOCIRecipe()
# Create oldest builds first so that they sort properly by id.
date_gen = time_counter(
- datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
+ datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+ )
builds = [
self.makeBuild(recipe=recipe, date_created=next(date_gen))
- for i in range(11)]
+ for i in range(11)
+ ]
view = OCIRecipeView(recipe, None)
self.assertEqual(list(reversed(builds)), view.builds)
self.setStatus(builds[10], BuildStatus.FULLYBUILT)
@@ -1713,7 +2072,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
# When there are >= 9 pending builds, only the most recent of any
# completed builds is returned.
self.assertEqual(
- list(reversed(builds[:9])) + [builds[10]], view.builds)
+ list(reversed(builds[:9])) + [builds[10]], view.builds
+ )
for build in builds[:9]:
self.setStatus(build, BuildStatus.FULLYBUILT)
del get_property_cache(view).builds
@@ -1721,34 +2081,46 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
-
def setUp(self):
super().setUp()
self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
self.distroseries = self.factory.makeDistroSeries(
- distribution=self.ubuntu, name="shiny", displayname="Shiny")
+ distribution=self.ubuntu, name="shiny", displayname="Shiny"
+ )
+ distribution = self.distroseries.distribution
self.architectures = []
for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
das = self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=architecture,
- processor=getUtility(IProcessorSet).getByName(processor))
+ distroseries=self.distroseries,
+ architecturetag=architecture,
+ processor=getUtility(IProcessorSet).getByName(processor),
+ )
das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
self.architectures.append(das)
- self.useFixture(FeatureFixture({
- OCI_RECIPE_ALLOW_CREATE: "on",
- "oci.build_series.%s" % self.distroseries.distribution.name:
- self.distroseries.name,
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s"
+ % distribution.name: self.distroseries.name,
+ }
+ )
+ )
oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution,
- ociprojectname="oci-project-name")
+ pillar=distribution,
+ ociprojectname="oci-project-name",
+ )
self.recipe = self.factory.makeOCIRecipe(
- name="recipe-name", registrant=self.person, owner=self.person,
- oci_project=oci_project)
+ name="recipe-name",
+ registrant=self.person,
+ owner=self.person,
+ oci_project=oci_project,
+ )
def test_request_builds_page(self):
# The +request-builds page is sane.
- self.assertTextMatchesExpressionIgnoreWhitespace("""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """
Request builds for recipe-name
recipe-name
Request builds
@@ -1758,12 +2130,14 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
or
Cancel
""",
- self.getMainText(self.recipe, "+request-builds", user=self.person))
+ self.getMainText(self.recipe, "+request-builds", user=self.person),
+ )
def test_request_builds_not_owner(self):
# A user without launchpad.Edit cannot request builds.
self.assertRaises(
- Unauthorized, self.getViewBrowser, self.recipe, "+request-builds")
+ Unauthorized, self.getViewBrowser, self.recipe, "+request-builds"
+ )
def runRequestBuildJobs(self):
with admin_logged_in():
@@ -1774,7 +2148,8 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
def test_request_builds_action(self):
# Requesting a build creates pending builds.
browser = self.getViewBrowser(
- self.recipe, "+request-builds", user=self.person)
+ self.recipe, "+request-builds", user=self.person
+ )
self.assertTrue(browser.getControl("amd64").selected)
self.assertTrue(browser.getControl("i386").selected)
browser.getControl("Request builds").click()
@@ -1785,146 +2160,206 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
builds = self.recipe.pending_builds
self.assertContentEqual(
["amd64", "i386"],
- [build.distro_arch_series.architecturetag for build in builds])
+ [build.distro_arch_series.architecturetag for build in builds],
+ )
self.assertContentEqual(
- [2510], {build.buildqueue_record.lastscore for build in builds})
+ [2510], {build.buildqueue_record.lastscore for build in builds}
+ )
def test_request_builds_no_architectures(self):
# Selecting no architectures causes a validation failure.
browser = self.getViewBrowser(
- self.recipe, "+request-builds", user=self.person)
+ self.recipe, "+request-builds", user=self.person
+ )
browser.getControl("amd64").selected = False
browser.getControl("i386").selected = False
browser.getControl("Request builds").click()
self.assertIn(
"You need to select at least one architecture.",
- extract_text(find_main_content(browser.contents)))
+ extract_text(find_main_content(browser.contents)),
+ )
-class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
- BaseTestOCIRecipeView):
+class TestOCIRecipeEditPushRulesView(
+ OCIConfigHelperMixin, BaseTestOCIRecipeView
+):
def setUp(self):
super().setUp()
self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
self.distroseries = self.factory.makeDistroSeries(
- distribution=self.ubuntu, name="shiny", displayname="Shiny")
-
- self.useFixture(FeatureFixture({
- OCI_RECIPE_ALLOW_CREATE: "on",
- "oci.build_series.%s" % self.distroseries.distribution.name:
- self.distroseries.name,
- }))
+ distribution=self.ubuntu, name="shiny", displayname="Shiny"
+ )
+ distribution = self.distroseries.distribution
+
+ self.useFixture(
+ FeatureFixture(
+ {
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s"
+ % distribution.name: self.distroseries.name,
+ }
+ )
+ )
self.oci_project = self.factory.makeOCIProject(
- pillar=self.distroseries.distribution,
- ociprojectname="oci-project-name")
+ pillar=distribution,
+ ociprojectname="oci-project-name",
+ )
self.member = self.factory.makePerson()
self.team = self.factory.makeTeam(members=[self.person, self.member])
self.recipe = self.factory.makeOCIRecipe(
- name="recipe-name", registrant=self.person, owner=self.person,
- oci_project=self.oci_project)
+ name="recipe-name",
+ registrant=self.person,
+ owner=self.person,
+ oci_project=self.oci_project,
+ )
self.team_owned_recipe = self.factory.makeOCIRecipe(
- name="recipe-name", registrant=self.person, owner=self.team,
- oci_project=self.oci_project)
+ name="recipe-name",
+ registrant=self.person,
+ owner=self.team,
+ oci_project=self.oci_project,
+ )
self.setConfig()
def test_view_oci_push_rules_owner(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
image_name = self.factory.getUniqueUnicode()
push_rule = getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
view = create_initialized_view(
- self.recipe, "+index", principal=self.person)
+ self.recipe, "+index", principal=self.person
+ )
# Display the Registry URL and the Username
# for the credentials owner
with person_logged_in(self.person):
rendered_view = view.render()
- row = soupmatchers.Tag("push rule row", "tr",
- attrs={"id": "rule-%d" % push_rule.id})
- self.assertThat(rendered_view, soupmatchers.HTMLContains(
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Registry URL", "td",
- text=registry_credentials.url)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Username", "td",
- text=registry_credentials.username)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "Image name", "td", text=image_name))))
+ row = soupmatchers.Tag(
+ "push rule row", "tr", attrs={"id": "rule-%d" % push_rule.id}
+ )
+ self.assertThat(
+ rendered_view,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Registry URL", "td", text=registry_credentials.url
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Username",
+ "td",
+ text=registry_credentials.username,
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag("Image name", "td", text=image_name),
+ ),
+ ),
+ )
def test_view_oci_push_rules_non_owner(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
image_name = self.factory.getUniqueUnicode()
push_rule = getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
non_owner = self.factory.makePerson()
admin = self.factory.makePerson(
- member_of=[getUtility(IPersonSet).getByName('admins')])
+ member_of=[getUtility(IPersonSet).getByName("admins")]
+ )
view = create_initialized_view(
- self.recipe, "+index", principal=non_owner)
+ self.recipe, "+index", principal=non_owner
+ )
# Display only the image name for users
# who are not the registry credentials owner
with person_logged_in(non_owner):
rendered_view = view.render()
- row = soupmatchers.Tag("push rule row", "tr",
- attrs={"id": "rule-%d" % push_rule.id})
- self.assertThat(rendered_view, soupmatchers.HTMLContains(
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Registry URL", "td",
- text=soupmatchers._not_passed)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Username", "td",
- text=soupmatchers._not_passed)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "Image name", "td", text=image_name))))
+ row = soupmatchers.Tag(
+ "push rule row", "tr", attrs={"id": "rule-%d" % push_rule.id}
+ )
+ self.assertThat(
+ rendered_view,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Registry URL", "td", text=soupmatchers._not_passed
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Username", "td", text=soupmatchers._not_passed
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag("Image name", "td", text=image_name),
+ ),
+ ),
+ )
# Anonymous users can't see registry credentials
# even though they can see the push rule
with anonymous_logged_in():
rendered_view = view.render()
- row = soupmatchers.Tag("push rule row", "tr",
- attrs={"id": "rule-%d" % push_rule.id})
- self.assertThat(rendered_view, soupmatchers.HTMLContains(
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Registry URL", "td",
- text=soupmatchers._not_passed)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Region", "td",
- text=soupmatchers._not_passed)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Username", "td",
- text=soupmatchers._not_passed)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "Image name", "td", text=image_name))))
+ row = soupmatchers.Tag(
+ "push rule row", "tr", attrs={"id": "rule-%d" % push_rule.id}
+ )
+ self.assertThat(
+ rendered_view,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Registry URL", "td", text=soupmatchers._not_passed
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Region", "td", text=soupmatchers._not_passed
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Username", "td", text=soupmatchers._not_passed
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag("Image name", "td", text=image_name),
+ ),
+ ),
+ )
# Although not the owner of the registry credentials
# the admin user has launchpad.View permission on
@@ -1933,56 +2368,76 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
# see ViewOCIRegistryCredentials
with person_logged_in(admin):
rendered_view = view.render()
- row = soupmatchers.Tag("push rule row", "tr",
- attrs={"id": "rule-%d" % push_rule.id})
- self.assertThat(rendered_view, soupmatchers.HTMLContains(
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Registry URL", "td",
- text=registry_credentials.url)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag("Username", "td",
- text=registry_credentials.username)),
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "Image name", "td", text=image_name))))
+ row = soupmatchers.Tag(
+ "push rule row", "tr", attrs={"id": "rule-%d" % push_rule.id}
+ )
+ self.assertThat(
+ rendered_view,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Registry URL", "td", text=registry_credentials.url
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "Username",
+ "td",
+ text=registry_credentials.username,
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag("Image name", "td", text=image_name),
+ ),
+ ),
+ )
def test_edit_oci_push_rules(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
image_name = self.factory.getUniqueUnicode()
push_rule = getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
# assert image name is displayed correctly
with person_logged_in(self.person):
- self.assertEqual(image_name, browser.getControl(
- name="field.image_name.%d" % push_rule.id).value)
+ self.assertEqual(
+ image_name,
+ browser.getControl(
+ name="field.image_name.%d" % push_rule.id
+ ).value,
+ )
# assert image name is required
with person_logged_in(self.person):
browser.getControl(
- name="field.image_name.%d" % push_rule.id).value = ""
+ name="field.image_name.%d" % push_rule.id
+ ).value = ""
browser.getControl("Save").click()
self.assertIn("Required input is missing", browser.contents)
# set image name to valid string
with person_logged_in(self.person):
browser.getControl(
- name="field.image_name.%d" % push_rule.id).value = "image1"
+ name="field.image_name.%d" % push_rule.id
+ ).value = "image1"
browser.getControl("Save").click()
# and assert model changed
with person_logged_in(self.person):
- self.assertEqual(
- push_rule.image_name, "image1")
+ self.assertEqual(push_rule.image_name, "image1")
# Create a second push rule and test we call setNewImageName only
# in cases where image name is different than the one on the model
# otherwise we get the exception on rows the user doesn't actually
@@ -1992,16 +2447,17 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
second_rule = getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name="second image")
+ image_name="second image",
+ )
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
with person_logged_in(self.person):
browser.getControl(
- name="field.image_name.%d" % push_rule.id).value = "image2"
+ name="field.image_name.%d" % push_rule.id
+ ).value = "image2"
browser.getControl("Save").click()
with person_logged_in(self.person):
- self.assertEqual(
- push_rule.image_name, "image2")
+ self.assertEqual(push_rule.image_name, "image2")
# Attempt to set the same name on the second rule
# will result in expected exception
@@ -2009,48 +2465,67 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
browser.getLink("Edit push rules").click()
with person_logged_in(self.person):
browser.getControl(
- name="field.image_name.%d" % second_rule.id).value = "image2"
- self.assertRaises(OCIPushRuleAlreadyExists,
- browser.getControl("Save").click)
+ name="field.image_name.%d" % second_rule.id
+ ).value = "image2"
+ self.assertRaises(
+ OCIPushRuleAlreadyExists, browser.getControl("Save").click
+ )
def test_edit_oci_push_rules_non_owner_of_credentials(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
image_names = [self.factory.getUniqueUnicode() for _ in range(2)]
push_rules = [
getUtility(IOCIPushRuleSet).new(
recipe=self.team_owned_recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
- for image_name in image_names]
+ image_name=image_name,
+ )
+ for image_name in image_names
+ ]
Store.of(push_rules[-1]).flush()
push_rule_ids = [push_rule.id for push_rule in push_rules]
browser = self.getViewBrowser(self.team_owned_recipe, user=self.member)
browser.getLink("Edit push rules").click()
row = soupmatchers.Tag(
- "push rule row", "tr", attrs={"class": "push-rule"})
- self.assertThat(browser.contents, soupmatchers.HTMLContains(
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "username widget", "span",
- attrs={
- "id": "field.username.%d" % push_rule_ids[0],
- "class": "sprite private",
- })),
- soupmatchers.Within(
- row,
- soupmatchers.Tag(
- "url widget", "span",
- attrs={
- "id": "field.url.%d" % push_rule_ids[0],
- "class": "sprite private",
- }))))
+ "push rule row", "tr", attrs={"class": "push-rule"}
+ )
+ self.assertThat(
+ browser.contents,
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "username widget",
+ "span",
+ attrs={
+ "id": "field.username.%d" % push_rule_ids[0],
+ "class": "sprite private",
+ },
+ ),
+ ),
+ soupmatchers.Within(
+ row,
+ soupmatchers.Tag(
+ "url widget",
+ "span",
+ attrs={
+ "id": "field.url.%d" % push_rule_ids[0],
+ "class": "sprite private",
+ },
+ ),
+ ),
+ ),
+ )
browser.getControl(
- name="field.image_name.%d" % push_rule_ids[0]).value = "image1"
+ name="field.image_name.%d" % push_rule_ids[0]
+ ).value = "image1"
browser.getControl("Save").click()
with person_logged_in(self.member):
self.assertEqual("image1", push_rules[0].image_name)
@@ -2058,25 +2533,31 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
def test_delete_oci_push_rules(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
image_name = self.factory.getUniqueUnicode()
push_rule = getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
with person_logged_in(self.person):
browser.getControl(
- name="field.delete.%d" % push_rule.id).value = True
+ name="field.delete.%d" % push_rule.id
+ ).value = True
browser.getControl("Save").click()
with person_logged_in(self.person):
self.assertIsNone(
- getUtility(IOCIPushRuleSet).getByID(push_rule.id))
+ getUtility(IOCIPushRuleSet).getByID(push_rule.id)
+ )
def test_add_oci_push_rules_validations(self):
# Add new rule works when there are no rules in the DB.
@@ -2113,20 +2594,28 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
browser.getControl(name="field.add_url").value = url
browser.getControl("Save").click()
with person_logged_in(self.person):
- rules = list(removeSecurityProxy(
- getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+ rules = list(
+ removeSecurityProxy(
+ getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)
+ )
+ )
self.assertEqual(len(rules), 1)
rule = rules[0]
- self.assertThat(rule, MatchesStructure(
- image_name=Equals("imagename1"),
- registry_url=Equals(url),
- registry_credentials=MatchesStructure(
- url=Equals(url),
- username=Is(None))))
+ self.assertThat(
+ rule,
+ MatchesStructure(
+ image_name=Equals("imagename1"),
+ registry_url=Equals(url),
+ registry_credentials=MatchesStructure(
+ url=Equals(url), username=Is(None)
+ ),
+ ),
+ )
with person_logged_in(self.person):
self.assertEqual(
- {"password": None}, rule.registry_credentials.getCredentials())
+ {"password": None}, rule.registry_credentials.getCredentials()
+ )
def test_add_oci_push_rules_new_username_password(self):
# Supplying an image name, registry URL, username, and password
@@ -2142,24 +2631,36 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
browser.getControl(name="field.add_username").value = "username"
browser.getControl(name="field.add_password").value = "password"
browser.getControl(
- name="field.add_confirm_password").value = "password"
+ name="field.add_confirm_password"
+ ).value = "password"
browser.getControl("Save").click()
with person_logged_in(self.person):
- rules = list(removeSecurityProxy(
- getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+ rules = list(
+ removeSecurityProxy(
+ getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)
+ )
+ )
self.assertEqual(len(rules), 1)
rule = rules[0]
- self.assertThat(rule, MatchesStructure(
- image_name=Equals("imagename3"),
- registry_url=Equals(url),
- registry_credentials=MatchesStructure.byEquality(
- url=url,
- username="username")))
+ self.assertThat(
+ rule,
+ MatchesStructure(
+ image_name=Equals("imagename3"),
+ registry_url=Equals(url),
+ registry_credentials=MatchesStructure.byEquality(
+ url=url, username="username"
+ ),
+ ),
+ )
with person_logged_in(self.person):
- self.assertEqual({
- "username": "username", "password": "password",
- "region": "somewhere-02"},
- rule.registry_credentials.getCredentials())
+ self.assertEqual(
+ {
+ "username": "username",
+ "password": "password",
+ "region": "somewhere-02",
+ },
+ rule.registry_credentials.getCredentials(),
+ )
def test_add_oci_push_rules_existing_credentials_duplicate(self):
# Adding a new push rule using existing credentials fails if a rule
@@ -2167,21 +2668,30 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
existing_rule = self.factory.makeOCIPushRule(
recipe=self.recipe,
registry_credentials=self.factory.makeOCIRegistryCredentials(
- registrant=self.recipe.owner, owner=self.recipe.owner))
+ registrant=self.recipe.owner, owner=self.recipe.owner
+ ),
+ )
existing_image_name = existing_rule.image_name
existing_registry_url = existing_rule.registry_url
existing_username = existing_rule.username
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
browser.getControl(name="field.add_credentials").value = "existing"
- browser.getControl(name="field.add_image_name").value = (
- existing_image_name)
- browser.getControl(name="field.existing_credentials").value = (
- "%s %s" % (quote(existing_registry_url), quote(existing_username)))
+ browser.getControl(
+ name="field.add_image_name"
+ ).value = existing_image_name
+ browser.getControl(
+ name="field.existing_credentials"
+ ).value = "%s %s" % (
+ quote(existing_registry_url),
+ quote(existing_username),
+ )
browser.getControl("Save").click()
self.assertIn(
"A push rule already exists with the same URL, "
- "image name, and credentials.", browser.contents)
+ "image name, and credentials.",
+ browser.contents,
+ )
def test_add_oci_push_rules_existing_credentials(self):
# Previously added registry credentials can be chosen from the radio
@@ -2192,27 +2702,38 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
existing_rule = self.factory.makeOCIPushRule(
recipe=self.recipe,
registry_credentials=self.factory.makeOCIRegistryCredentials(
- registrant=self.recipe.owner, owner=self.recipe.owner,
- credentials={}))
+ registrant=self.recipe.owner,
+ owner=self.recipe.owner,
+ credentials={},
+ ),
+ )
existing_registry_url = existing_rule.registry_url
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
browser.getControl(name="field.add_credentials").value = "existing"
browser.getControl(name="field.add_image_name").value = "imagename2"
- browser.getControl(name="field.existing_credentials").value = (
- quote(existing_registry_url))
+ browser.getControl(name="field.existing_credentials").value = quote(
+ existing_registry_url
+ )
browser.getControl("Save").click()
with person_logged_in(self.person):
- rules = list(removeSecurityProxy(
- getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+ rules = list(
+ removeSecurityProxy(
+ getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)
+ )
+ )
self.assertEqual(len(rules), 2)
rule = rules[1]
- self.assertThat(rule, MatchesStructure(
- image_name=Equals("imagename2"),
- registry_url=Equals(existing_registry_url),
- registry_credentials=MatchesStructure(
- url=Equals(existing_registry_url),
- username=Is(None))))
+ self.assertThat(
+ rule,
+ MatchesStructure(
+ image_name=Equals("imagename2"),
+ registry_url=Equals(existing_registry_url),
+ registry_credentials=MatchesStructure(
+ url=Equals(existing_registry_url), username=Is(None)
+ ),
+ ),
+ )
with person_logged_in(self.person):
self.assertEqual({}, rule.registry_credentials.getCredentials())
@@ -2220,40 +2741,44 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
url = self.factory.getUniqueURL()
browser = self.getViewBrowser(self.team_owned_recipe, user=self.member)
browser.getLink("Edit push rules").click()
- browser.getControl(
- name="field.add_image_name").value = "imagename1"
- browser.getControl(
- name="field.add_url").value = url
+ browser.getControl(name="field.add_image_name").value = "imagename1"
+ browser.getControl(name="field.add_url").value = url
browser.getControl(name="field.add_credentials").value = "new"
browser.getControl("Save").click()
with person_logged_in(self.member):
- rules = list(removeSecurityProxy(
- getUtility(IOCIPushRuleSet).findByRecipe(
- self.team_owned_recipe)))
+ rules = list(
+ removeSecurityProxy(
+ getUtility(IOCIPushRuleSet).findByRecipe(
+ self.team_owned_recipe
+ )
+ )
+ )
self.assertEqual(len(rules), 1)
rule = rules[0]
- self.assertThat(rule, MatchesStructure(
- image_name=Equals('imagename1'),
- registry_url=Equals(url),
- registry_credentials=MatchesStructure(
- url=Equals(url),
- username=Is(None))))
+ self.assertThat(
+ rule,
+ MatchesStructure(
+ image_name=Equals("imagename1"),
+ registry_url=Equals(url),
+ registry_credentials=MatchesStructure(
+ url=Equals(url), username=Is(None)
+ ),
+ ),
+ )
with person_logged_in(self.member):
self.assertThat(
rule.registry_credentials.getCredentials(),
- MatchesDict(
- {"password": Equals(None)}))
+ MatchesDict({"password": Equals(None)}),
+ )
def test_edit_oci_push_rules_team_owned(self):
url = self.factory.getUniqueURL()
browser = self.getViewBrowser(self.team_owned_recipe, user=self.member)
browser.getLink("Edit push rules").click()
- browser.getControl(
- name="field.add_image_name").value = "imagename1"
- browser.getControl(
- name="field.add_url").value = url
+ browser.getControl(name="field.add_image_name").value = "imagename1"
+ browser.getControl(name="field.add_url").value = url
browser.getControl(name="field.add_credentials").value = "new"
browser.getControl("Save").click()
@@ -2262,43 +2787,55 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
browser = self.getViewBrowser(self.team_owned_recipe, user=self.person)
browser.getLink("Edit push rules").click()
with person_logged_in(self.person):
- rules = list(removeSecurityProxy(
- getUtility(IOCIPushRuleSet).findByRecipe(
- self.team_owned_recipe)))
+ rules = list(
+ removeSecurityProxy(
+ getUtility(IOCIPushRuleSet).findByRecipe(
+ self.team_owned_recipe
+ )
+ )
+ )
self.assertEqual(len(rules), 1)
rule = rules[0]
- self.assertEqual("imagename1", browser.getControl(
- name="field.image_name.%d" % rule.id).value)
+ self.assertEqual(
+ "imagename1",
+ browser.getControl(name="field.image_name.%d" % rule.id).value,
+ )
# set image name to valid string
with person_logged_in(self.person):
browser.getControl(
- name="field.image_name.%d" % rule.id).value = "image1"
+ name="field.image_name.%d" % rule.id
+ ).value = "image1"
browser.getControl("Save").click()
# and assert model changed
with person_logged_in(self.member):
- self.assertEqual(
- rule.image_name, "image1")
+ self.assertEqual(rule.image_name, "image1")
# self.member will see the new image name
browser = self.getViewBrowser(self.team_owned_recipe, user=self.member)
browser.getLink("Edit push rules").click()
with person_logged_in(self.member):
- self.assertEqual("image1", browser.getControl(
- name="field.image_name.%d" % rule.id).value)
+ self.assertEqual(
+ "image1",
+ browser.getControl(name="field.image_name.%d" % rule.id).value,
+ )
def test_edit_oci_registry_creds(self):
url = self.factory.getUniqueURL()
- credentials = {'username': 'foo', 'password': 'bar'}
+ credentials = {"username": "foo", "password": "bar"}
image_name = self.factory.getUniqueUnicode()
registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
- registrant=self.person, owner=self.person, url=url,
- credentials=credentials)
+ registrant=self.person,
+ owner=self.person,
+ url=url,
+ credentials=credentials,
+ )
getUtility(IOCIPushRuleSet).new(
recipe=self.recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
browser = self.getViewBrowser(self.recipe, user=self.person)
browser.getLink("Edit push rules").click()
browser.getLink("Edit OCI registry credentials").click()
@@ -2307,22 +2844,27 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
browser.getControl(name="field.add_region").value = "new_region1"
browser.getControl(name="field.add_username").value = "new_username"
browser.getControl(name="field.add_password").value = "password"
- browser.getControl(name="field.add_confirm_password"
- ).value = "password"
+ browser.getControl(
+ name="field.add_confirm_password"
+ ).value = "password"
browser.getControl("Save").click()
with person_logged_in(self.person):
- creds = list(getUtility(
- IOCIRegistryCredentialsSet).findByOwner(
- self.person))
+ creds = list(
+ getUtility(IOCIRegistryCredentialsSet).findByOwner(self.person)
+ )
self.assertEqual(url, creds[1].url)
self.assertThat(
(creds[1]).getCredentials(),
- MatchesDict({
- "username": Equals("new_username"),
- "password": Equals("password"),
- "region": Equals("new_region1")}))
+ MatchesDict(
+ {
+ "username": Equals("new_username"),
+ "password": Equals("password"),
+ "region": Equals("new_region1"),
+ }
+ ),
+ )
class TestOCIRecipeListingView(BaseTestOCIRecipeView):
@@ -2330,32 +2872,42 @@ class TestOCIRecipeListingView(BaseTestOCIRecipeView):
super().setUp()
self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
self.distroseries = self.factory.makeDistroSeries(
- distribution=self.ubuntu, name="shiny", displayname="Shiny")
+ distribution=self.ubuntu, name="shiny", displayname="Shiny"
+ )
self.architectures = []
for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
das = self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=architecture,
- processor=getUtility(IProcessorSet).getByName(processor))
+ distroseries=self.distroseries,
+ architecturetag=architecture,
+ processor=getUtility(IProcessorSet).getByName(processor),
+ )
das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
self.architectures.append(das)
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
self.oci_project = self.factory.makeOCIProject(
pillar=self.distroseries.distribution,
- ociprojectname="oci-project-name")
+ ociprojectname="oci-project-name",
+ )
def makeRecipes(self, count=1, **kwargs):
with person_logged_in(self.person):
owner = self.factory.makePerson()
- return [self.factory.makeOCIRecipe(
- registrant=owner, owner=owner, oci_project=self.oci_project,
- **kwargs)
- for _ in range(count)]
+ return [
+ self.factory.makeOCIRecipe(
+ registrant=owner,
+ owner=owner,
+ oci_project=self.oci_project,
+ **kwargs,
+ )
+ for _ in range(count)
+ ]
def test_oci_recipe_list_for_person(self):
owner = self.factory.makePerson(name="recipe-owner")
for i in range(2):
self.factory.makeOCIRecipe(
- name="my-oci-recipe-%s" % i, owner=owner, registrant=owner)
+ name="my-oci-recipe-%s" % i, owner=owner, registrant=owner
+ )
# This recipe should not be present.
someone_else = self.factory.makePerson()
@@ -2373,23 +2925,30 @@ class TestOCIRecipeListingView(BaseTestOCIRecipeView):
my-oci-recipe-1 Recipe-owner .*
1 .* 2 of 2 results
First .* Previous .* Next .* Last
- """, main_text)
+ """,
+ main_text,
+ )
def test_shows_no_recipe(self):
"""Should shows correct message when there are no visible recipes."""
# Create a private OCI recipe that should not be shown.
owner = self.factory.makePerson()
self.factory.makeOCIRecipe(
- owner=owner, registrant=owner, oci_project=self.oci_project,
- information_type=InformationType.PRIVATESECURITY)
+ owner=owner,
+ registrant=owner,
+ oci_project=self.oci_project,
+ information_type=InformationType.PRIVATESECURITY,
+ )
browser = self.getViewBrowser(
- self.oci_project, "+recipes", user=self.person)
+ self.oci_project, "+recipes", user=self.person
+ )
main_text = extract_text(find_main_content(browser.contents))
with person_logged_in(self.person):
self.assertIn(
"There are no recipes registered for %s"
% self.oci_project.name,
- main_text)
+ main_text,
+ )
def test_paginates_recipes(self):
batch_size = 5
@@ -2397,26 +2956,29 @@ class TestOCIRecipeListingView(BaseTestOCIRecipeView):
# We will create 1 private recipe with proper permission in the
# list, and 9 others. This way, we should have 10 recipes in the list.
[private_recipe] = self.makeRecipes(
- 1, information_type=InformationType.PRIVATESECURITY)
+ 1, information_type=InformationType.PRIVATESECURITY
+ )
with admin_logged_in():
private_recipe.subscribe(self.person, private_recipe.owner)
recipes = self.makeRecipes(9)
recipes.append(private_recipe)
browser = self.getViewBrowser(
- self.oci_project, "+recipes", user=self.person)
+ self.oci_project, "+recipes", user=self.person
+ )
main_text = extract_text(find_main_content(browser.contents))
- no_wrap_main_text = main_text.replace('\n', ' ')
+ no_wrap_main_text = main_text.replace("\n", " ")
with person_logged_in(self.person):
self.assertIn(
"There are 10 recipes registered for %s"
% self.oci_project.name,
- no_wrap_main_text)
+ no_wrap_main_text,
+ )
self.assertIn("1 → 5 of 10 results", no_wrap_main_text)
self.assertIn("First • Previous • Next • Last", no_wrap_main_text)
# Make sure it's listing the first set of recipes
- items = sorted(recipes, key=attrgetter('name'))
+ items = sorted(recipes, key=attrgetter("name"))
for recipe in items[:batch_size]:
self.assertIn(recipe.name, main_text)
@@ -2426,15 +2988,17 @@ class TestOCIRecipeListingView(BaseTestOCIRecipeView):
def getView():
view = self.getViewBrowser(
- self.oci_project, "+recipes", user=self.person)
+ self.oci_project, "+recipes", user=self.person
+ )
return view
def do_login():
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
login_person(self.person)
recorder1, recorder2 = record_two_runs(
- getView, self.makeRecipes, 1, 15, login_method=do_login)
+ getView, self.makeRecipes, 1, 15, login_method=do_login
+ )
# The first run (with no extra pages) makes BatchNavigator issue one
# extra count(*) on OCIRecipe. Shouldn't be a big deal.
diff --git a/lib/lp/oci/browser/tests/test_ocirecipebuild.py b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
index 2466f85..5d622e4 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
@@ -5,11 +5,11 @@
import re
-from fixtures import FakeLogger
import soupmatchers
+import transaction
+from fixtures import FakeLogger
from storm.locals import Store
from testtools.matchers import StartsWith
-import transaction
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -25,19 +25,16 @@ from lp.services.webapp import canonical_url
from lp.testing import (
ANONYMOUS,
BrowserTestCase,
+ TestCaseWithFactory,
login,
person_logged_in,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.pages import (
extract_text,
find_main_content,
find_tags_by_class,
- )
+)
from lp.testing.views import create_initialized_view
@@ -47,22 +44,28 @@ class TestCanonicalUrlForOCIRecipeBuild(TestCaseWithFactory):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_canonical_url(self):
owner = self.factory.makePerson(name="person")
distribution = self.factory.makeDistribution(name="distro")
oci_project = self.factory.makeOCIProject(
- pillar=distribution, ociprojectname="oci-project")
+ pillar=distribution, ociprojectname="oci-project"
+ )
recipe = self.factory.makeOCIRecipe(
- name="recipe", registrant=owner, owner=owner,
- oci_project=oci_project)
+ name="recipe",
+ registrant=owner,
+ owner=owner,
+ oci_project=oci_project,
+ )
build = self.factory.makeOCIRecipeBuild(requester=owner, recipe=recipe)
self.assertThat(
canonical_url(build),
StartsWith(
"http://launchpad.test/~person/distro/+oci/oci-project/"
- "+recipe/recipe/+build/"))
+ "+recipe/recipe/+build/"
+ ),
+ )
class TestOCIRecipeBuildView(BrowserTestCase):
@@ -71,13 +74,14 @@ class TestOCIRecipeBuildView(BrowserTestCase):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_index(self):
build = self.factory.makeOCIRecipeBuild()
recipe = build.recipe
oci_project = recipe.oci_project
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
386 build of .*
created .*
Build status
@@ -85,10 +89,15 @@ class TestOCIRecipeBuildView(BrowserTestCase):
Build details
Recipe: OCI recipe %s/%s/%s for %s
Architecture: i386
- """ % (
- oci_project.pillar.name, oci_project.name, recipe.name,
- recipe.owner.display_name),
- self.getMainText(build))
+ """
+ % (
+ oci_project.pillar.name,
+ oci_project.name,
+ recipe.name,
+ recipe.owner.display_name,
+ ),
+ self.getMainText(build),
+ )
def test_files(self):
# OCIRecipeBuildView.files returns all the associated files.
@@ -97,7 +106,8 @@ class TestOCIRecipeBuildView(BrowserTestCase):
build_view = create_initialized_view(build, "+index")
self.assertEqual(
[oci_file.library_file.filename],
- [lfa.filename for lfa in build_view.files])
+ [lfa.filename for lfa in build_view.files],
+ )
# Deleted files won't be included.
self.assertFalse(oci_file.library_file.deleted)
removeSecurityProxy(oci_file.library_file).content = None
@@ -109,11 +119,17 @@ class TestOCIRecipeBuildView(BrowserTestCase):
build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
getUtility(IOCIRegistryUploadJobSource).create(build)
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "registry upload status", "li",
- attrs={"id": "registry-upload-status"},
- text=re.compile(r"^\s*Registry upload in progress\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "registry upload status",
+ "li",
+ attrs={"id": "registry-upload-status"},
+ text=re.compile(r"^\s*Registry upload in progress\s*$"),
+ )
+ ),
+ )
def test_registry_upload_status_completed(self):
build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
@@ -121,35 +137,50 @@ class TestOCIRecipeBuildView(BrowserTestCase):
naked_job = removeSecurityProxy(job)
naked_job.job._status = JobStatus.COMPLETED
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "registry upload status", "li",
- attrs={"id": "registry-upload-status"},
- text=re.compile(r"^\s*Registry upload complete\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "registry upload status",
+ "li",
+ attrs={"id": "registry-upload-status"},
+ text=re.compile(r"^\s*Registry upload complete\s*$"),
+ )
+ ),
+ )
def test_registry_upload_status_failed(self):
build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
job = getUtility(IOCIRegistryUploadJobSource).create(build)
naked_job = removeSecurityProxy(job)
naked_job.job._status = JobStatus.FAILED
- naked_job.error_summary = (
- "Upload of test-digest for test-image failed")
+ naked_job.error_summary = "Upload of test-digest for test-image failed"
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Within(
- soupmatchers.Tag(
- "registry upload status", "li",
- attrs={"id": "registry-upload-status"},
- text=re.compile(
- r"^\s*Registry upload failed:\s+"
- r"Upload of test-digest for test-image failed\s*$")),
- soupmatchers.Tag(
- "retry button", "input",
- attrs={
- "type": "submit",
- "name": "field.actions.upload",
- "value": "Retry",
- }))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Within(
+ soupmatchers.Tag(
+ "registry upload status",
+ "li",
+ attrs={"id": "registry-upload-status"},
+ text=re.compile(
+ r"^\s*Registry upload failed:\s+"
+ r"Upload of test-digest for test-image failed\s*$"
+ ),
+ ),
+ soupmatchers.Tag(
+ "retry button",
+ "input",
+ attrs={
+ "type": "submit",
+ "name": "field.actions.upload",
+ "value": "Retry",
+ },
+ ),
+ )
+ ),
+ )
class TestOCIRecipeBuildOperations(BrowserTestCase):
@@ -159,12 +190,13 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
def setUp(self):
super().setUp()
self.useFixture(FakeLogger())
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
self.build = self.factory.makeOCIRecipeBuild()
self.build_url = canonical_url(self.build)
self.requester = self.build.requester
self.buildd_admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
+ member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]
+ )
def test_retry_build(self):
# The requester of a build can retry it.
@@ -185,10 +217,14 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
user = self.factory.makePerson()
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Retry this build")
+ LinkNotFoundError, browser.getLink, "Retry this build"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+retry",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+retry",
+ user=user,
+ )
def test_retry_build_wrong_state(self):
# If the build isn't in an unsuccessful terminal state, you can't
@@ -196,7 +232,8 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
self.build.updateStatus(BuildStatus.FULLYBUILT)
browser = self.getViewBrowser(self.build, user=self.requester)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Retry this build")
+ LinkNotFoundError, browser.getLink, "Retry this build"
+ )
def test_cancel_build(self):
# The requester of a build can cancel it.
@@ -218,8 +255,11 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+cancel",
+ user=user,
+ )
def test_cancel_build_wrong_state(self):
# If the build isn't queued, you can't cancel it.
@@ -250,7 +290,8 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
browser.getControl("Rescore build").click()
self.assertEqual(
"Invalid integer data",
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
def test_rescore_build_not_admin(self):
# A non-admin user cannot cancel a build.
@@ -260,8 +301,11 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+rescore",
+ user=user,
+ )
def test_rescore_build_wrong_state(self):
# If the build isn't NEEDSBUILD, you can't rescore it.
@@ -278,22 +322,32 @@ class TestOCIRecipeBuildOperations(BrowserTestCase):
with person_logged_in(self.requester):
self.build.cancel()
browser = self.getViewBrowser(
- self.build, "+rescore", user=self.buildd_admin)
+ self.build, "+rescore", user=self.buildd_admin
+ )
self.assertEqual(self.build_url, browser.url)
- self.assertThat(browser.contents, soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "notification", "div", attrs={"class": "warning message"},
- text="Cannot rescore this build because it is not queued.")))
+ self.assertThat(
+ browser.contents,
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "notification",
+ "div",
+ attrs={"class": "warning message"},
+ text="Cannot rescore this build because it is not queued.",
+ )
+ ),
+ )
def test_builder_history(self):
Store.of(self.build).flush()
self.build.updateStatus(
- BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
+ BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder()
+ )
title = self.build.title
browser = self.getViewBrowser(self.build.builder, "+history")
self.assertTextMatchesExpressionIgnoreWhitespace(
"Build history.*%s" % re.escape(title),
- extract_text(find_main_content(browser.contents)))
+ extract_text(find_main_content(browser.contents)),
+ )
self.assertEqual(self.build_url, browser.getLink(title).url)
def makeBuildingOCIRecipe(self):
diff --git a/lib/lp/oci/browser/tests/test_ocirecipesubscription.py b/lib/lp/oci/browser/tests/test_ocirecipesubscription.py
index fbd60d6..34362cb 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipesubscription.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipesubscription.py
@@ -10,18 +10,14 @@ from lp.app.enums import InformationType
from lp.oci.tests.helpers import OCIConfigHelperMixin
from lp.registry.enums import BranchSharingPolicy
from lp.services.webapp import canonical_url
-from lp.testing import (
- admin_logged_in,
- BrowserTestCase,
- person_logged_in,
- )
+from lp.testing import BrowserTestCase, admin_logged_in, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.pages import (
extract_text,
find_main_content,
find_tag_by_id,
find_tags_by_class,
- )
+)
class BaseTestOCIRecipeView(OCIConfigHelperMixin, BrowserTestCase):
@@ -32,55 +28,74 @@ class BaseTestOCIRecipeView(OCIConfigHelperMixin, BrowserTestCase):
super().setUp()
self.setConfig()
self.useFixture(FakeLogger())
- self.person = self.factory.makePerson(name='recipe-owner')
+ self.person = self.factory.makePerson(name="recipe-owner")
def makeOCIRecipe(self, oci_project=None, **kwargs):
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person, name="recipe-repository",
- paths=["refs/heads/v1.0-20.04"])
+ owner=self.person,
+ target=self.person,
+ name="recipe-repository",
+ paths=["refs/heads/v1.0-20.04"],
+ )
if oci_project is None:
project = self.factory.makeProduct(
- owner=self.person, registrant=self.person)
+ owner=self.person, registrant=self.person
+ )
oci_project = self.factory.makeOCIProject(
- registrant=self.person, pillar=project,
- ociprojectname='my-oci-project')
+ registrant=self.person,
+ pillar=project,
+ ociprojectname="my-oci-project",
+ )
return self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person, name="recipe-name",
- git_ref=ref, oci_project=oci_project, **kwargs)
+ registrant=self.person,
+ owner=self.person,
+ name="recipe-name",
+ git_ref=ref,
+ oci_project=oci_project,
+ **kwargs,
+ )
def getSubscriptionPortletText(self, browser):
return extract_text(
- find_tag_by_id(browser.contents, 'portlet-subscribers'))
+ find_tag_by_id(browser.contents, "portlet-subscribers")
+ )
def extractMainText(self, browser):
return extract_text(find_main_content(browser.contents))
def extractInfoMessageContent(self, browser):
return extract_text(
- find_tags_by_class(browser.contents, 'informational message')[0])
+ find_tags_by_class(browser.contents, "informational message")[0]
+ )
class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
-
def test_subscribe_self(self):
recipe = self.makeOCIRecipe()
another_user = self.factory.makePerson(name="another-user")
browser = self.getViewBrowser(recipe, user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Subscribe yourself
Subscribe someone else
Subscribers
Recipe-owner
- """, self.getSubscriptionPortletText(browser))
+ """,
+ self.getSubscriptionPortletText(browser),
+ )
# Go to "subscribe myself" page, and click the button.
browser = self.getViewBrowser(
- recipe, view_name="+subscribe", user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ recipe, view_name="+subscribe", user=another_user
+ )
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Subscribe to OCI recipe
recipe-name
Subscribe to OCI recipe or Cancel
- """, self.extractMainText(browser))
+ """,
+ self.extractMainText(browser),
+ )
browser.getControl("Subscribe").click()
# We should be redirected back to OCI page.
@@ -88,13 +103,16 @@ class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
self.assertEqual(canonical_url(recipe), browser.url)
# And the new user should be listed in the subscribers list.
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit your subscription
Subscribe someone else
Subscribers
Another-user
Recipe-owner
- """, self.getSubscriptionPortletText(browser))
+ """,
+ self.getSubscriptionPortletText(browser),
+ )
def test_unsubscribe_self(self):
recipe = self.makeOCIRecipe()
@@ -103,15 +121,21 @@ class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
recipe.subscribe(another_user, recipe.owner)
subscription = recipe.getSubscription(another_user)
browser = self.getViewBrowser(subscription, user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit subscription to OCI recipe for Another-user
If you unsubscribe from an OCI recipe it will no longer show up on
your personal pages. or Cancel
- """, self.extractMainText(browser))
+ """,
+ self.extractMainText(browser),
+ )
browser.getControl("Unsubscribe").click()
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Another-user has been unsubscribed from this OCI recipe.
- """, self.extractInfoMessageContent(browser))
+ """,
+ self.extractInfoMessageContent(browser),
+ )
with person_logged_in(self.person):
self.assertIsNone(recipe.getSubscription(another_user))
@@ -119,25 +143,32 @@ class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
recipe = self.makeOCIRecipe()
another_user = self.factory.makePerson(name="another-user")
browser = self.getViewBrowser(recipe, user=recipe.owner)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit your subscription
Subscribe someone else
Subscribers
Recipe-owner
- """, self.getSubscriptionPortletText(browser))
+ """,
+ self.getSubscriptionPortletText(browser),
+ )
# Go to "subscribe" page, and click the button.
browser = self.getViewBrowser(
- recipe, view_name="+addsubscriber", user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ recipe, view_name="+addsubscriber", user=another_user
+ )
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Subscribe to OCI recipe
Person:
.*
The person subscribed to the related OCI recipe.
or
Cancel
- """, self.extractMainText(browser))
- browser.getControl(name="field.person").value = 'another-user'
+ """,
+ self.extractMainText(browser),
+ )
+ browser.getControl(name="field.person").value = "another-user"
browser.getControl("Subscribe").click()
# We should be redirected back to OCI recipe page.
@@ -145,13 +176,16 @@ class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
self.assertEqual(canonical_url(recipe), browser.url)
# And the new user should be listed in the subscribers list.
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit your subscription
Subscribe someone else
Subscribers
Another-user
Recipe-owner
- """, self.getSubscriptionPortletText(browser))
+ """,
+ self.getSubscriptionPortletText(browser),
+ )
def test_unsubscribe_someone_else(self):
recipe = self.makeOCIRecipe()
@@ -161,45 +195,63 @@ class TestPublicOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
subscription = recipe.getSubscription(another_user)
browser = self.getViewBrowser(subscription, user=recipe.owner)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit subscription to OCI recipe for Another-user
If you unsubscribe from an OCI recipe it will no longer show up on
your personal pages. or Cancel
- """, self.extractMainText(browser))
+ """,
+ self.extractMainText(browser),
+ )
browser.getControl("Unsubscribe").click()
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Another-user has been unsubscribed from this OCI recipe.
- """, self.extractInfoMessageContent(browser))
+ """,
+ self.extractInfoMessageContent(browser),
+ )
with person_logged_in(self.person):
self.assertIsNone(recipe.getSubscription(another_user))
class TestPrivateOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
-
def makePrivateOCIRecipe(self, **kwargs):
project = self.factory.makeProduct(
- owner=self.person, registrant=self.person,
+ owner=self.person,
+ registrant=self.person,
information_type=InformationType.PROPRIETARY,
- branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY,
+ )
oci_project = self.factory.makeOCIProject(
- ociprojectname='my-oci-project', pillar=project)
+ ociprojectname="my-oci-project", pillar=project
+ )
return self.makeOCIRecipe(
information_type=InformationType.PROPRIETARY,
- oci_project=oci_project)
+ oci_project=oci_project,
+ )
def test_cannot_subscribe_to_private_snap(self):
recipe = self.makePrivateOCIRecipe()
another_user = self.factory.makePerson(name="another-user")
# Unsubscribed user should not see the OCI recipe page.
self.assertRaises(
- Unauthorized, self.getViewBrowser, recipe, user=another_user)
+ Unauthorized, self.getViewBrowser, recipe, user=another_user
+ )
# Nor the subscribe pages.
self.assertRaises(
- Unauthorized, self.getViewBrowser,
- recipe, view_name="+subscribe", user=another_user)
+ Unauthorized,
+ self.getViewBrowser,
+ recipe,
+ view_name="+subscribe",
+ user=another_user,
+ )
self.assertRaises(
- Unauthorized, self.getViewBrowser,
- recipe, view_name="+addsubscriber", user=another_user)
+ Unauthorized,
+ self.getViewBrowser,
+ recipe,
+ view_name="+addsubscriber",
+ user=another_user,
+ )
def test_recipe_owner_can_subscribe_someone_to_private_recipe(self):
recipe = self.makePrivateOCIRecipe()
@@ -207,28 +259,35 @@ class TestPrivateOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
# Go to "subscribe" page, and click the button.
browser = self.getViewBrowser(
- recipe, view_name="+addsubscriber", user=self.person)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ recipe, view_name="+addsubscriber", user=self.person
+ )
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Subscribe to OCI recipe
Person:
.*
The person subscribed to the related OCI recipe.
or
Cancel
- """, self.extractMainText(browser))
- browser.getControl(name="field.person").value = 'another-user'
+ """,
+ self.extractMainText(browser),
+ )
+ browser.getControl(name="field.person").value = "another-user"
browser.getControl("Subscribe").click()
# Now the new user should be listed in the subscribers list,
# and have access to the recipe page.
browser = self.getViewBrowser(recipe, user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit your subscription
Subscribe someone else
Subscribers
Another-user
Recipe-owner
- """, self.getSubscriptionPortletText(browser))
+ """,
+ self.getSubscriptionPortletText(browser),
+ )
def test_unsubscribe_self(self):
recipe = self.makePrivateOCIRecipe()
@@ -237,14 +296,20 @@ class TestPrivateOCIRecipeSubscriptionViews(BaseTestOCIRecipeView):
recipe.subscribe(another_user, self.person)
subscription = recipe.getSubscription(another_user)
browser = self.getViewBrowser(subscription, user=another_user)
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Edit subscription to OCI recipe for Another-user
If you unsubscribe from an OCI recipe it will no longer show up on
your personal pages. or Cancel
- """, self.extractMainText(browser))
+ """,
+ self.extractMainText(browser),
+ )
browser.getControl("Unsubscribe").click()
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Another-user has been unsubscribed from this OCI recipe.
- """, self.extractInfoMessageContent(browser))
+ """,
+ self.extractInfoMessageContent(browser),
+ )
with person_logged_in(self.person):
self.assertIsNone(recipe.getSubscription(another_user))
diff --git a/lib/lp/oci/enums.py b/lib/lp/oci/enums.py
index c0cb164..b41c5a7 100644
--- a/lib/lp/oci/enums.py
+++ b/lib/lp/oci/enums.py
@@ -4,32 +4,35 @@
"""Enums for the OCI app."""
__all__ = [
- 'OCIRecipeBuildRequestStatus',
- ]
+ "OCIRecipeBuildRequestStatus",
+]
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
class OCIRecipeBuildRequestStatus(EnumeratedType):
"""The status of a request to build an OCI recipe."""
- PENDING = Item("""
+ PENDING = Item(
+ """
Pending
This OCI recipe build request is pending.
- """)
+ """
+ )
- FAILED = Item("""
+ FAILED = Item(
+ """
Failed
This OCI recipe build request failed.
- """)
+ """
+ )
- COMPLETED = Item("""
+ COMPLETED = Item(
+ """
Completed
This OCI recipe build request completed successfully.
- """)
+ """
+ )
diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
index 9c800fa..8a90b2b 100644
--- a/lib/lp/oci/interfaces/ocipushrule.py
+++ b/lib/lp/oci/interfaces/ocipushrule.py
@@ -4,10 +4,10 @@
"""Interfaces for handling credentials for OCI registry actions."""
__all__ = [
- 'IOCIPushRule',
- 'IOCIPushRuleSet',
- 'OCIPushRuleAlreadyExists',
- ]
+ "IOCIPushRule",
+ "IOCIPushRuleSet",
+ "OCIPushRuleAlreadyExists",
+]
import http.client
@@ -20,13 +20,10 @@ from lazr.restful.declarations import (
mutator_for,
operation_for_version,
operation_parameters,
- )
+)
from lazr.restful.fields import Reference
from zope.interface import Interface
-from zope.schema import (
- Int,
- TextLine,
- )
+from zope.schema import Int, TextLine
from lp import _
from lp.oci.interfaces.ocirecipe import IOCIRecipe
@@ -36,13 +33,14 @@ from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
@error_status(http.client.CONFLICT)
class OCIPushRuleAlreadyExists(Exception):
"""A new OCIPushRuleAlreadyExists was added with the
- same details as an existing one.
+ same details as an existing one.
"""
def __init__(self):
super().__init__(
"A push rule already exists with the same URL, image name, "
- "and credentials")
+ "and credentials"
+ )
class IOCIPushRuleView(Interface):
@@ -52,18 +50,25 @@ class IOCIPushRuleView(Interface):
id = Int(title=_("ID"), required=False, readonly=True)
- registry_url = exported(TextLine(
- title=_("Registry URL"),
- description=_(
- "The registry URL for the credentials of this push rule"),
- required=True,
- readonly=True))
+ registry_url = exported(
+ TextLine(
+ title=_("Registry URL"),
+ description=_(
+ "The registry URL for the credentials of this push rule"
+ ),
+ required=True,
+ readonly=True,
+ )
+ )
- username = exported(TextLine(
- title=_("Username"),
- description=_("The username for the credentials, if available."),
- required=True,
- readonly=True))
+ username = exported(
+ TextLine(
+ title=_("Username"),
+ description=_("The username for the credentials, if available."),
+ required=True,
+ readonly=True,
+ )
+ )
class IOCIPushRuleEditableAttributes(Interface):
@@ -77,24 +82,30 @@ class IOCIPushRuleEditableAttributes(Interface):
title=_("OCI recipe"),
description=_("The recipe for which the rule is defined."),
required=True,
- readonly=False)
+ readonly=False,
+ )
registry_credentials = Reference(
IOCIRegistryCredentials,
title=_("Registry credentials"),
description=_("The registry credentials to use."),
required=True,
- readonly=False)
+ readonly=False,
+ )
- image_name = exported(TextLine(
- title=_("Image name"),
- description=_("The intended name of the image on the registry."),
- required=True,
- readonly=True))
+ image_name = exported(
+ TextLine(
+ title=_("Image name"),
+ description=_("The intended name of the image on the registry."),
+ required=True,
+ readonly=True,
+ )
+ )
@mutator_for(image_name)
@operation_parameters(
- image_name=TextLine(title=_("Image name"), required=True))
+ image_name=TextLine(title=_("Image name"), required=True)
+ )
@export_write_operation()
@operation_for_version("devel")
def setNewImageName(image_name):
@@ -113,9 +124,11 @@ class IOCIPushRuleEdit(Interface):
@exported_as_webservice_entry(
- publish_web_link=True, as_of="devel", singular_name="oci_push_rule")
-class IOCIPushRule(IOCIPushRuleEdit, IOCIPushRuleEditableAttributes,
- IOCIPushRuleView):
+ publish_web_link=True, as_of="devel", singular_name="oci_push_rule"
+)
+class IOCIPushRule(
+ IOCIPushRuleEdit, IOCIPushRuleEditableAttributes, IOCIPushRuleView
+):
"""A rule for pushing builds of an OCI recipe to a registry."""
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index 2a7256c..516a0c3 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -4,31 +4,32 @@
"""Interfaces related to recipes for OCI Images."""
__all__ = [
- 'CannotModifyOCIRecipeProcessor',
- 'DuplicateOCIRecipeName',
- 'IOCIRecipe',
- 'IOCIRecipeBuildRequest',
- 'IOCIRecipeEdit',
- 'IOCIRecipeEditableAttributes',
- 'IOCIRecipeSet',
- 'IOCIRecipeView',
- 'NoSourceForOCIRecipe',
- 'NoSuchOCIRecipe',
- 'OCIRecipeBuildAlreadyPending',
- 'OCIRecipeFeatureDisabled',
- 'OCIRecipeBranchHasInvalidFormat',
- 'OCIRecipeNotOwner',
- 'OCIRecipePrivacyMismatch',
- 'OCI_RECIPE_ALLOW_CREATE',
- 'OCI_RECIPE_BUILD_DISTRIBUTION',
- 'OCI_RECIPE_WEBHOOKS_FEATURE_FLAG',
- 'UsingDistributionCredentials',
- ]
+ "CannotModifyOCIRecipeProcessor",
+ "DuplicateOCIRecipeName",
+ "IOCIRecipe",
+ "IOCIRecipeBuildRequest",
+ "IOCIRecipeEdit",
+ "IOCIRecipeEditableAttributes",
+ "IOCIRecipeSet",
+ "IOCIRecipeView",
+ "NoSourceForOCIRecipe",
+ "NoSuchOCIRecipe",
+ "OCIRecipeBuildAlreadyPending",
+ "OCIRecipeFeatureDisabled",
+ "OCIRecipeBranchHasInvalidFormat",
+ "OCIRecipeNotOwner",
+ "OCIRecipePrivacyMismatch",
+ "OCI_RECIPE_ALLOW_CREATE",
+ "OCI_RECIPE_BUILD_DISTRIBUTION",
+ "OCI_RECIPE_WEBHOOKS_FEATURE_FLAG",
+ "UsingDistributionCredentials",
+]
import http.client
from lazr.lifecycle.snapshot import doNotSnapshot
from lazr.restful.declarations import (
+ REQUEST_USER,
call_with,
error_status,
export_factory_operation,
@@ -37,17 +38,9 @@ from lazr.restful.declarations import (
exported_as_webservice_entry,
operation_for_version,
operation_parameters,
- REQUEST_USER,
- )
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- ReferenceChoice,
- )
-from zope.interface import (
- Attribute,
- Interface,
- )
+)
+from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
+from zope.interface import Attribute, Interface
from zope.schema import (
Bool,
Choice,
@@ -58,7 +51,7 @@ from zope.schema import (
Set,
Text,
TextLine,
- )
+)
from zope.security.interfaces import Unauthorized
from lp import _
@@ -76,16 +69,12 @@ from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.role import IHasOwner
from lp.services.database.constants import DEFAULT
-from lp.services.fields import (
- PersonChoice,
- PublicPersonChoice,
- )
+from lp.services.fields import PersonChoice, PublicPersonChoice
from lp.services.webhooks.interfaces import IWebhookTarget
-
OCI_RECIPE_WEBHOOKS_FEATURE_FLAG = "oci.recipe.webhooks.enabled"
-OCI_RECIPE_ALLOW_CREATE = 'oci.recipe.create.enabled'
-OCI_RECIPE_BUILD_DISTRIBUTION = 'oci.default_build_distribution'
+OCI_RECIPE_ALLOW_CREATE = "oci.recipe.create.enabled"
+OCI_RECIPE_BUILD_DISTRIBUTION = "oci.default_build_distribution"
OCI_RECIPE_PRIVATE_FEATURE_FLAG = "oci.recipe.allow_private"
@@ -95,7 +84,8 @@ class OCIRecipeFeatureDisabled(Unauthorized):
def __init__(self):
super().__init__(
- "You do not have permission to create new OCI recipes.")
+ "You do not have permission to create new OCI recipes."
+ )
@error_status(http.client.UNAUTHORIZED)
@@ -109,7 +99,8 @@ class OCIRecipeBuildAlreadyPending(Exception):
def __init__(self):
super().__init__(
- "An identical build of this OCI recipe is already pending.")
+ "An identical build of this OCI recipe is already pending."
+ )
@error_status(http.client.BAD_REQUEST)
@@ -119,6 +110,7 @@ class DuplicateOCIRecipeName(Exception):
class NoSuchOCIRecipe(NameLookupFailed):
"""The requested OCI Recipe does not exist."""
+
_message_prefix = "No such OCI recipe exists for this OCI project"
@@ -128,7 +120,8 @@ class UsingDistributionCredentials(Exception):
def __init__(self):
super().__init__(
- "The OCI recipe is in a distribution that has credentials set.")
+ "The OCI recipe is in a distribution that has credentials set."
+ )
@error_status(http.client.BAD_REQUEST)
@@ -137,7 +130,8 @@ class NoSourceForOCIRecipe(Exception):
def __init__(self):
super().__init__(
- "New OCI recipes must have a git branch and build file.")
+ "New OCI recipes must have a git branch and build file."
+ )
@error_status(http.client.FORBIDDEN)
@@ -145,11 +139,12 @@ class CannotModifyOCIRecipeProcessor(Exception):
"""Tried to enable or disable a restricted processor on an OCI recipe."""
_fmt = (
- '%(processor)s is restricted, and may only be enabled or disabled '
- 'by administrators.')
+ "%(processor)s is restricted, and may only be enabled or disabled "
+ "by administrators."
+ )
def __init__(self, processor):
- super().__init__(self._fmt % {'processor': processor.name})
+ super().__init__(self._fmt % {"processor": processor.name})
@error_status(http.client.BAD_REQUEST)
@@ -158,8 +153,9 @@ class OCIRecipePrivacyMismatch(Exception):
def __init__(self, message=None):
super().__init__(
- message or
- "OCI recipe contains private information and cannot be public.")
+ message
+ or "OCI recipe contains private information and cannot be public."
+ )
@error_status(http.client.BAD_REQUEST)
@@ -169,50 +165,86 @@ class OCIRecipeBranchHasInvalidFormat(Exception):
def __init__(self):
super().__init__(
"The branch name for the OCI recipe does not "
- "match the APPVERSION-UBUNTUVERSION format (ex. v1.0-20.04)")
+ "match the APPVERSION-UBUNTUVERSION format (ex. v1.0-20.04)"
+ )
+
@exported_as_webservice_entry(
- publish_web_link=True, as_of="devel",
- singular_name="oci_recipe_build_request")
+ publish_web_link=True,
+ as_of="devel",
+ singular_name="oci_recipe_build_request",
+)
class IOCIRecipeBuildRequest(Interface):
"""A request to build an OCI Recipe."""
id = Int(title=_("ID"), required=True, readonly=True)
- date_requested = exported(Datetime(
- title=_("The time when this request was made"),
- required=True, readonly=True))
+ date_requested = exported(
+ Datetime(
+ title=_("The time when this request was made"),
+ required=True,
+ readonly=True,
+ )
+ )
- date_finished = exported(Datetime(
- title=_("The time when this request finished"),
- required=False, readonly=True))
+ date_finished = exported(
+ Datetime(
+ title=_("The time when this request finished"),
+ required=False,
+ readonly=True,
+ )
+ )
- recipe = exported(Reference(
- # Really IOCIRecipe, patched in lp.oci.interfaces.webservice.
- Interface,
- title=_("OCI Recipe"), required=True, readonly=True))
+ recipe = exported(
+ Reference(
+ # Really IOCIRecipe, patched in lp.oci.interfaces.webservice.
+ Interface,
+ title=_("OCI Recipe"),
+ required=True,
+ readonly=True,
+ )
+ )
- status = exported(Choice(
- title=_("Status"), vocabulary=OCIRecipeBuildRequestStatus,
- required=True, readonly=True))
+ status = exported(
+ Choice(
+ title=_("Status"),
+ vocabulary=OCIRecipeBuildRequestStatus,
+ required=True,
+ readonly=True,
+ )
+ )
- error_message = exported(TextLine(
- title=_("Error message"), required=False, readonly=True))
+ error_message = exported(
+ TextLine(title=_("Error message"), required=False, readonly=True)
+ )
- builds = exported(doNotSnapshot(CollectionField(
- title=_("Builds produced by this request"),
- # Really IOCIRecipeBuild, patched in lp.oci.interfaces.webservice.
- value_type=Reference(schema=Interface),
- required=True, readonly=True)))
+ builds = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("Builds produced by this request"),
+ # Really IOCIRecipeBuild, patched in
+ # lp.oci.interfaces.webservice.
+ value_type=Reference(schema=Interface),
+ required=True,
+ readonly=True,
+ )
+ )
+ )
architectures = Set(
title=_("If set, limit builds to these architecture tags."),
- value_type=TextLine(), required=False, readonly=True)
+ value_type=TextLine(),
+ required=False,
+ readonly=True,
+ )
uploaded_manifests = Dict(
title=_("A dict of manifest information per build."),
- key_type=Int(), value_type=Dict(),
- required=False, readonly=True)
+ key_type=Int(),
+ value_type=Dict(),
+ required=False,
+ readonly=True,
+ )
def addUploadedManifest(build_id, manifest_info):
"""Add the manifest information for one of the builds in this
@@ -224,51 +256,69 @@ class IOCIRecipeView(Interface):
"""`IOCIRecipe` attributes that require launchpad.View permission."""
id = Int(title=_("ID"), required=True, readonly=True)
- date_created = exported(Datetime(
- title=_("Date created"), required=True, readonly=True))
- date_last_modified = exported(Datetime(
- title=_("Date last modified"), required=True, readonly=True))
+ date_created = exported(
+ Datetime(title=_("Date created"), required=True, readonly=True)
+ )
+ date_last_modified = exported(
+ Datetime(title=_("Date last modified"), required=True, readonly=True)
+ )
- registrant = exported(PublicPersonChoice(
- title=_("Registrant"),
- description=_("The user who registered this recipe."),
- vocabulary='ValidPersonOrTeam', required=True, readonly=True))
+ registrant = exported(
+ PublicPersonChoice(
+ title=_("Registrant"),
+ description=_("The user who registered this recipe."),
+ vocabulary="ValidPersonOrTeam",
+ required=True,
+ readonly=True,
+ )
+ )
distribution = Reference(
- IDistribution, title=_("Distribution"),
- required=True, readonly=True,
- description=_("The distribution that this recipe is associated with."))
+ IDistribution,
+ title=_("Distribution"),
+ required=True,
+ readonly=True,
+ description=_("The distribution that this recipe is associated with."),
+ )
distro_series = Reference(
- IDistroSeries, title=_("Distro series"),
- required=True, readonly=True,
- description=_("The series for which the recipe should be built."))
+ IDistroSeries,
+ title=_("Distro series"),
+ required=True,
+ readonly=True,
+ description=_("The series for which the recipe should be built."),
+ )
available_processors = Attribute(
"The architectures that are available to be enabled or disabled for "
- "this OCI recipe.")
+ "this OCI recipe."
+ )
pending_build_requests = Attribute(
- "The list of build requests that didn't trigger builds yet.")
+ "The list of build requests that didn't trigger builds yet."
+ )
# This should only be set by using IOCIProject.setOfficialRecipe
official = Bool(
title=_("OCI project official"),
required=False,
description=_("True if this recipe is official for its OCI project."),
- readonly=True)
+ readonly=True,
+ )
private = Bool(
title=_("Is this OCI recipe private?"),
- required=True, readonly=True,
- description=_("True if this recipe is private. False otherwise."))
+ required=True,
+ readonly=True,
+ description=_("True if this recipe is private. False otherwise."),
+ )
- pillar = Attribute('The pillar of this OCI recipe.')
+ pillar = Attribute("The pillar of this OCI recipe.")
@call_with(check_permissions=True, user=REQUEST_USER)
@operation_parameters(
- processors=List(
- value_type=Reference(schema=IProcessor), required=True))
+ processors=List(value_type=Reference(schema=IProcessor), required=True)
+ )
@export_write_operation()
@operation_for_version("devel")
def setProcessors(processors, check_permissions=False, user=None):
@@ -284,66 +334,101 @@ class IOCIRecipeView(Interface):
title=_("Completed builds of this OCI recipe."),
description=_(
"Completed builds of this OCI recipe, sorted in descending "
- "order of finishing."),
+ "order of finishing."
+ ),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
value_type=Reference(schema=Interface),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
completed_builds = CollectionField(
title=_("Completed builds of this OCI recipe."),
description=_(
"Completed builds of this OCI recipe, sorted in descending "
- "order of finishing."),
+ "order of finishing."
+ ),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
- value_type=Reference(schema=Interface), readonly=True)
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
completed_builds_without_build_request = CollectionField(
title=_("Completed builds of this OCI recipe."),
description=_(
"Completed builds of this OCI recipe, sorted in descending "
"order of finishing that do no have a corresponding "
- "build request"),
+ "build request"
+ ),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
- value_type=Reference(schema=Interface), readonly=True)
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
pending_builds = CollectionField(
title=_("Pending builds of this OCI recipe."),
description=_(
"Pending builds of this OCI recipe, sorted in descending "
- "order of creation."),
+ "order of creation."
+ ),
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
- value_type=Reference(schema=Interface), readonly=True)
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
- push_rules = exported(CollectionField(
- title=_("Push rules for this OCI recipe."),
- description=_("All of the push rules for registry upload "
- "that apply to this recipe."),
- # Really IOCIPushRule, patched in _schema_cirular_imports.
- value_type=Reference(schema=Interface), readonly=True))
+ push_rules = exported(
+ CollectionField(
+ title=_("Push rules for this OCI recipe."),
+ description=_(
+ "All of the push rules for registry upload "
+ "that apply to this recipe."
+ ),
+ # Really IOCIPushRule, patched in _schema_cirular_imports.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+ )
can_upload_to_registry = Bool(
- title=_("Can upload to registry"), required=True, readonly=True,
+ title=_("Can upload to registry"),
+ required=True,
+ readonly=True,
description=_(
"Whether everything is set up to allow uploading builds of "
- "this OCI recipe to a registry."))
+ "this OCI recipe to a registry."
+ ),
+ )
is_valid_branch_format = Bool(
- title=_("Is valid branch format"), required=True, readonly=True,
- description=_("Whether the git branch name is the correct "
- "format for using as a tag name."))
+ title=_("Is valid branch format"),
+ required=True,
+ readonly=True,
+ description=_(
+ "Whether the git branch name is the correct "
+ "format for using as a tag name."
+ ),
+ )
use_distribution_credentials = Bool(
- title=_("Use Distribution credentials"), required=True, readonly=True,
- description=_("Use the credentials on a Distribution for "
- "registry upload"))
+ title=_("Use Distribution credentials"),
+ required=True,
+ readonly=True,
+ description=_(
+ "Use the credentials on a Distribution for " "registry upload"
+ ),
+ )
subscriptions = CollectionField(
title=_("OCIRecipeSubscriptions associated with this OCI recipe."),
- readonly=True, value_type=Reference(Interface))
+ readonly=True,
+ value_type=Reference(Interface),
+ )
subscribers = CollectionField(
title=_("Persons subscribed to this snap recipe."),
- readonly=True, value_type=Reference(IPerson))
+ readonly=True,
+ value_type=Reference(IPerson),
+ )
def requestBuild(requester, architecture):
"""Request that the OCI recipe is built.
@@ -377,8 +462,7 @@ class IOCIRecipeView(Interface):
"""
def getBuildRequest(job_id):
- """Get an OCIRecipeBuildRequest object for the given job_id.
- """
+ """Get an OCIRecipeBuildRequest object for the given job_id."""
def getAllowedInformationTypes(user):
"""Get a list of acceptable `InformationType`s for this OCI recipe.
@@ -413,25 +497,36 @@ class IOCIRecipeEdit(IWebhookTarget):
registry_url=TextLine(
title=_("Registry URL"),
description=_("URL for the target registry"),
- required=True),
+ required=True,
+ ),
image_name=TextLine(
title=_("Image name"),
description=_("Name of the image to push to on the registry"),
- required=True),
+ required=True,
+ ),
credentials=Dict(
title=_("Registry credentials"),
description=_(
- "The credentials to use in pushing the image to the registry"),
- required=True),
+ "The credentials to use in pushing the image to the registry"
+ ),
+ required=True,
+ ),
credentials_owner=PersonChoice(
title=_("Registry credentials owner"),
required=False,
- vocabulary="AllUserTeamsParticipationPlusSelf"))
+ vocabulary="AllUserTeamsParticipationPlusSelf",
+ ),
+ )
# Really IOCIPushRule, patched in lp.oci.interfaces.webservice.
@export_factory_operation(Interface, [])
@operation_for_version("devel")
- def newPushRule(registrant, registry_url, image_name, credentials,
- credentials_owner=None):
+ def newPushRule(
+ registrant,
+ registry_url,
+ image_name,
+ credentials,
+ credentials_owner=None,
+ ):
"""Add a new rule for pushing builds of this recipe to a registry."""
@@ -441,98 +536,155 @@ class IOCIRecipeEditableAttributes(IHasOwner):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- name = exported(TextLine(
- title=_("Name"),
- description=_("The name of this recipe."),
- constraint=name_validator,
- required=True,
- readonly=False))
+ name = exported(
+ TextLine(
+ title=_("Name"),
+ description=_("The name of this recipe."),
+ constraint=name_validator,
+ required=True,
+ readonly=False,
+ )
+ )
- owner = exported(PersonChoice(
- title=_("Owner"),
- required=True,
- vocabulary="AllUserTeamsParticipationPlusSelf",
- description=_("The owner of this OCI recipe."),
- readonly=False))
+ owner = exported(
+ PersonChoice(
+ title=_("Owner"),
+ required=True,
+ vocabulary="AllUserTeamsParticipationPlusSelf",
+ description=_("The owner of this OCI recipe."),
+ readonly=False,
+ )
+ )
- information_type = exported(Choice(
- title=_("Information type"), vocabulary=InformationType,
- required=True, readonly=False, default=InformationType.PUBLIC,
- description=_(
- "The type of information contained in this OCI recipe.")))
+ information_type = exported(
+ Choice(
+ title=_("Information type"),
+ vocabulary=InformationType,
+ required=True,
+ readonly=False,
+ default=InformationType.PUBLIC,
+ description=_(
+ "The type of information contained in this OCI recipe."
+ ),
+ )
+ )
- oci_project = exported(Reference(
- IOCIProject,
- title=_("OCI project"),
- description=_("The OCI project that this recipe is for."),
- required=True,
- readonly=True))
+ oci_project = exported(
+ Reference(
+ IOCIProject,
+ title=_("OCI project"),
+ description=_("The OCI project that this recipe is for."),
+ required=True,
+ readonly=True,
+ )
+ )
- git_ref = exported(Reference(
- IGitRef, title=_("Git branch"), required=True, readonly=False,
- description=_(
- "The Git branch containing a Dockerfile at the location "
- "defined by the build_file attribute.")))
+ git_ref = exported(
+ Reference(
+ IGitRef,
+ title=_("Git branch"),
+ required=True,
+ readonly=False,
+ description=_(
+ "The Git branch containing a Dockerfile at the location "
+ "defined by the build_file attribute."
+ ),
+ )
+ )
git_repository = ReferenceChoice(
title=_("Git repository"),
- schema=IGitRepository, vocabulary="GitRepository",
- required=False, readonly=False,
+ schema=IGitRepository,
+ vocabulary="GitRepository",
+ required=False,
+ readonly=False,
description=_(
"A Git repository with a branch containing a Dockerfile "
- "at the location defined by the build_file attribute."))
+ "at the location defined by the build_file attribute."
+ ),
+ )
git_path = TextLine(
- title=_("Git branch path"), required=True, readonly=False,
+ title=_("Git branch path"),
+ required=True,
+ readonly=False,
description=_(
"The path of the Git branch containing a Dockerfile "
- "at the location defined by the build_file attribute."))
+ "at the location defined by the build_file attribute."
+ ),
+ )
- description = exported(Text(
- title=_("Description"),
- description=_("A short description of this recipe."),
- required=False,
- readonly=False))
+ description = exported(
+ Text(
+ title=_("Description"),
+ description=_("A short description of this recipe."),
+ required=False,
+ readonly=False,
+ )
+ )
- build_file = exported(TextLine(
- title=_("Build file path"),
- description=_("The relative path to the file within this recipe's "
- "branch that defines how to build the recipe."),
- constraint=path_does_not_escape,
- required=True,
- readonly=False))
-
- build_args = exported(Dict(
- title=_("Build ARG variables"),
- description=_("The dictionary of ARG variables to be used when "
- "building this recipe."),
- key_type=TextLine(title=_("ARG name")),
- value_type=TextLine(title=_("ARG value")),
- required=False,
- readonly=False))
+ build_file = exported(
+ TextLine(
+ title=_("Build file path"),
+ description=_(
+ "The relative path to the file within this recipe's "
+ "branch that defines how to build the recipe."
+ ),
+ constraint=path_does_not_escape,
+ required=True,
+ readonly=False,
+ )
+ )
- build_path = exported(TextLine(
- title=_("Build directory context"),
- description=_("Directory to use for build context "
- "and OCIRecipe.build_file location."),
- constraint=path_does_not_escape,
- required=True,
- readonly=False))
+ build_args = exported(
+ Dict(
+ title=_("Build ARG variables"),
+ description=_(
+ "The dictionary of ARG variables to be used when "
+ "building this recipe."
+ ),
+ key_type=TextLine(title=_("ARG name")),
+ value_type=TextLine(title=_("ARG value")),
+ required=False,
+ readonly=False,
+ )
+ )
- build_daily = exported(Bool(
- title=_("Build daily"),
- required=True,
- default=False,
- description=_("If True, this recipe should be built daily."),
- readonly=False))
-
- image_name = exported(TextLine(
- title=_("Image name"),
- description=_("Image name to use on upload to registry. "
- "Defaults to recipe name if not set. "
- "Only used when Distribution credentials are set."),
- required=False,
- readonly=False))
+ build_path = exported(
+ TextLine(
+ title=_("Build directory context"),
+ description=_(
+ "Directory to use for build context "
+ "and OCIRecipe.build_file location."
+ ),
+ constraint=path_does_not_escape,
+ required=True,
+ readonly=False,
+ )
+ )
+
+ build_daily = exported(
+ Bool(
+ title=_("Build daily"),
+ required=True,
+ default=False,
+ description=_("If True, this recipe should be built daily."),
+ readonly=False,
+ )
+ )
+
+ image_name = exported(
+ TextLine(
+ title=_("Image name"),
+ description=_(
+ "Image name to use on upload to registry. "
+ "Defaults to recipe name if not set. "
+ "Only used when Distribution credentials are set."
+ ),
+ required=False,
+ readonly=False,
+ )
+ )
class IOCIRecipeAdminAttributes(Interface):
@@ -542,39 +694,68 @@ class IOCIRecipeAdminAttributes(Interface):
"""
require_virtualized = Bool(
- title=_("Require virtualized builders"), required=True, readonly=False,
- description=_("Only build this OCI recipe on virtual builders."))
+ title=_("Require virtualized builders"),
+ required=True,
+ readonly=False,
+ description=_("Only build this OCI recipe on virtual builders."),
+ )
- processors = exported(CollectionField(
- title=_("Processors"),
- description=_(
- "The architectures for which the OCI recipe should be built."),
- value_type=Reference(schema=IProcessor),
- readonly=False))
+ processors = exported(
+ CollectionField(
+ title=_("Processors"),
+ description=_(
+ "The architectures for which the OCI recipe should be built."
+ ),
+ value_type=Reference(schema=IProcessor),
+ readonly=False,
+ )
+ )
- allow_internet = exported(Bool(
- title=_("Allow external network access"),
- required=True, readonly=False,
- description=_(
- "Allow access to external network resources via a proxy. "
- "Resources hosted on Launchpad itself are always allowed.")))
+ allow_internet = exported(
+ Bool(
+ title=_("Allow external network access"),
+ required=True,
+ readonly=False,
+ description=_(
+ "Allow access to external network resources via a proxy. "
+ "Resources hosted on Launchpad itself are always allowed."
+ ),
+ )
+ )
@exported_as_webservice_entry(
- publish_web_link=True, as_of="devel", singular_name="oci_recipe")
-class IOCIRecipe(IOCIRecipeView, IOCIRecipeEdit, IOCIRecipeEditableAttributes,
- IOCIRecipeAdminAttributes):
+ publish_web_link=True, as_of="devel", singular_name="oci_recipe"
+)
+class IOCIRecipe(
+ IOCIRecipeView,
+ IOCIRecipeEdit,
+ IOCIRecipeEditableAttributes,
+ IOCIRecipeAdminAttributes,
+):
"""A recipe for building Open Container Initiative images."""
class IOCIRecipeSet(Interface):
"""A utility to create and access OCI Recipes."""
- def new(name, registrant, owner, oci_project, git_ref, build_file,
- description=None, official=False, require_virtualized=True,
- build_daily=False, processors=None, date_created=DEFAULT,
- allow_internet=True, build_args=None,
- information_type=InformationType.PUBLIC):
+ def new(
+ name,
+ registrant,
+ owner,
+ oci_project,
+ git_ref,
+ build_file,
+ description=None,
+ official=False,
+ require_virtualized=True,
+ build_daily=False,
+ processors=None,
+ date_created=DEFAULT,
+ allow_internet=True,
+ build_args=None,
+ information_type=InformationType.PUBLIC,
+ ):
"""Create an IOCIRecipe."""
def exists(owner, oci_project, name):
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
index a43082b..7f9c71a 100644
--- a/lib/lp/oci/interfaces/ocirecipebuild.py
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -4,21 +4,18 @@
"""Interfaces for a build record for OCI recipes."""
__all__ = [
- 'CannotScheduleRegistryUpload',
- 'IOCIFile',
- 'IOCIFileSet',
- 'IOCIRecipeBuild',
- 'IOCIRecipeBuildSet',
- 'OCIRecipeBuildRegistryUploadStatus',
- 'OCIRecipeBuildSetRegistryUploadStatus',
- ]
+ "CannotScheduleRegistryUpload",
+ "IOCIFile",
+ "IOCIFileSet",
+ "IOCIRecipeBuild",
+ "IOCIRecipeBuildSet",
+ "OCIRecipeBuildRegistryUploadStatus",
+ "OCIRecipeBuildSetRegistryUploadStatus",
+]
import http.client
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
from lazr.restful.declarations import (
error_status,
export_read_operation,
@@ -26,24 +23,10 @@ from lazr.restful.declarations import (
exported,
exported_as_webservice_entry,
operation_for_version,
- )
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- )
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Bool,
- Choice,
- Datetime,
- Dict,
- Int,
- List,
- TextLine,
- )
+)
+from lazr.restful.fields import CollectionField, Reference
+from zope.interface import Attribute, Interface
+from zope.schema import Bool, Choice, Datetime, Dict, Int, List, TextLine
from lp import _
from lp.app.interfaces.launchpad import IPrivacy
@@ -51,15 +34,12 @@ from lp.buildmaster.interfaces.buildfarmjob import (
IBuildFarmJobAdmin,
IBuildFarmJobEdit,
ISpecificBuildFarmJobSource,
- )
+)
from lp.buildmaster.interfaces.packagebuild import (
IPackageBuild,
IPackageBuildView,
- )
-from lp.oci.interfaces.ocirecipe import (
- IOCIRecipe,
- IOCIRecipeBuildRequest,
- )
+)
+from lp.oci.interfaces.ocirecipe import IOCIRecipe, IOCIRecipeBuildRequest
from lp.services.database.constants import DEFAULT
from lp.services.fields import PublicPersonChoice
from lp.services.librarian.interfaces import ILibraryFileAlias
@@ -78,36 +58,46 @@ class OCIRecipeBuildRegistryUploadStatus(EnumeratedType):
that process.
"""
- UNSCHEDULED = Item("""
+ UNSCHEDULED = Item(
+ """
Unscheduled
No upload of this OCI build to a registry is scheduled.
- """)
+ """
+ )
- PENDING = Item("""
+ PENDING = Item(
+ """
Pending
This OCI build is queued for upload to a registry.
- """)
+ """
+ )
- FAILEDTOUPLOAD = Item("""
+ FAILEDTOUPLOAD = Item(
+ """
Failed to upload
The last attempt to upload this OCI build to a registry failed.
- """)
+ """
+ )
- UPLOADED = Item("""
+ UPLOADED = Item(
+ """
Uploaded
This OCI build was successfully uploaded to a registry.
- """)
+ """
+ )
- SUPERSEDED = Item("""
+ SUPERSEDED = Item(
+ """
Superseded
The upload has been cancelled because another build will upload a
more recent version.
- """)
+ """
+ )
class OCIRecipeBuildSetRegistryUploadStatus(EnumeratedType):
@@ -117,35 +107,45 @@ class OCIRecipeBuildSetRegistryUploadStatus(EnumeratedType):
that process.
"""
- UNSCHEDULED = Item("""
+ UNSCHEDULED = Item(
+ """
Unscheduled
No upload of these OCI builds to a registry is scheduled.
- """)
+ """
+ )
- PENDING = Item("""
+ PENDING = Item(
+ """
Pending
These OCI builds are queued for upload to a registry.
- """)
+ """
+ )
- FAILEDTOUPLOAD = Item("""
+ FAILEDTOUPLOAD = Item(
+ """
Failed to upload
The last attempt to upload these OCI builds to a registry failed.
- """)
+ """
+ )
- UPLOADED = Item("""
+ UPLOADED = Item(
+ """
Uploaded
These OCI builds were successfully uploaded to a registry.
- """)
+ """
+ )
- PARTIAL = Item("""
+ PARTIAL = Item(
+ """
Partial
Some OCI builds have uploaded to a registry.
- """)
+ """
+ )
class IOCIRecipeBuildView(IPackageBuildView, IPrivacy):
@@ -154,30 +154,51 @@ class IOCIRecipeBuildView(IPackageBuildView, IPrivacy):
build_request = Reference(
IOCIRecipeBuildRequest,
title=_("The build request that caused this build to be created."),
- required=False, readonly=True)
+ required=False,
+ readonly=True,
+ )
- requester = exported(PublicPersonChoice(
- title=_("Requester"),
- description=_("The person who requested this OCI recipe build."),
- vocabulary='ValidPersonOrTeam', required=True, readonly=True))
+ requester = exported(
+ PublicPersonChoice(
+ title=_("Requester"),
+ description=_("The person who requested this OCI recipe build."),
+ vocabulary="ValidPersonOrTeam",
+ required=True,
+ readonly=True,
+ )
+ )
- recipe = exported(Reference(
- IOCIRecipe,
- title=_("The OCI recipe to build."),
- required=True,
- readonly=True))
+ recipe = exported(
+ Reference(
+ IOCIRecipe,
+ title=_("The OCI recipe to build."),
+ required=True,
+ readonly=True,
+ )
+ )
- eta = exported(Datetime(
- title=_("The datetime when the build job is estimated to complete."),
- readonly=True))
+ eta = exported(
+ Datetime(
+ title=_(
+ "The datetime when the build job is estimated to complete."
+ ),
+ readonly=True,
+ )
+ )
- estimate = exported(Bool(
- title=_("If true, the date value is an estimate."), readonly=True))
+ estimate = exported(
+ Bool(title=_("If true, the date value is an estimate."), readonly=True)
+ )
- date = exported(Datetime(
- title=_(
- "The date when the build completed or is estimated to complete."),
- readonly=True))
+ date = exported(
+ Datetime(
+ title=_(
+ "The date when the build completed or is estimated to "
+ "complete."
+ ),
+ readonly=True,
+ )
+ )
def getFiles():
"""Retrieve the build's `IOCIFile` records.
@@ -213,17 +234,26 @@ class IOCIRecipeBuildView(IPackageBuildView, IPrivacy):
:return: The corresponding `ILibraryFileAlias`.
"""
- distro_arch_series = exported(Reference(
- IDistroArchSeries,
- title=_("The series and architecture for which to build."),
- required=True, readonly=True))
+ distro_arch_series = exported(
+ Reference(
+ IDistroArchSeries,
+ title=_("The series and architecture for which to build."),
+ required=True,
+ readonly=True,
+ )
+ )
arch_tag = exported(
- TextLine(title=_("Architecture tag"), required=True, readonly=True))
+ TextLine(title=_("Architecture tag"), required=True, readonly=True)
+ )
- score = exported(Int(
- title=_("Score of the related build farm job (if any)."),
- required=False, readonly=True))
+ score = exported(
+ Int(
+ title=_("Score of the related build farm job (if any)."),
+ required=False,
+ readonly=True,
+ )
+ )
manifest = Attribute(_("The manifest of the image."))
@@ -233,33 +263,48 @@ class IOCIRecipeBuildView(IPackageBuildView, IPrivacy):
title=_("Registry upload jobs for this build."),
# Really IOCIRegistryUploadJob.
value_type=Reference(schema=Interface),
- readonly=True)
+ readonly=True,
+ )
# Really IOCIRegistryUploadJob
last_registry_upload_job = Reference(
- title=_("Last registry upload job for this build."), schema=Interface)
-
- registry_upload_status = exported(Choice(
- title=_("Registry upload status"),
- vocabulary=OCIRecipeBuildRegistryUploadStatus,
- required=True, readonly=False
- ))
-
- registry_upload_error_summary = exported(TextLine(
- title=_("Registry upload error summary"),
- description=_(
- "The error summary, if any, from the last attempt to upload this "
- "build to a registry."),
- required=False, readonly=True))
-
- registry_upload_errors = exported(List(
- title=_("Detailed registry upload errors"),
- description=_(
- "A list of errors, as described in "
- "https://docs.docker.com/registry/spec/api/#errors, from the last "
- "attempt to upload this build to a registry."),
- value_type=Dict(key_type=TextLine()),
- required=False, readonly=True))
+ title=_("Last registry upload job for this build."), schema=Interface
+ )
+
+ registry_upload_status = exported(
+ Choice(
+ title=_("Registry upload status"),
+ vocabulary=OCIRecipeBuildRegistryUploadStatus,
+ required=True,
+ readonly=False,
+ )
+ )
+
+ registry_upload_error_summary = exported(
+ TextLine(
+ title=_("Registry upload error summary"),
+ description=_(
+ "The error summary, if any, from the last attempt to upload "
+ "this build to a registry."
+ ),
+ required=False,
+ readonly=True,
+ )
+ )
+
+ registry_upload_errors = exported(
+ List(
+ title=_("Detailed registry upload errors"),
+ description=_(
+ "A list of errors, as described in "
+ "https://docs.docker.com/registry/spec/api/#errors, from the "
+ "last attempt to upload this build to a registry."
+ ),
+ value_type=Dict(key_type=TextLine()),
+ required=False,
+ readonly=True,
+ )
+ )
def hasMoreRecentBuild():
"""Checks if this recipe has a more recent build currently building or
@@ -295,17 +340,21 @@ class IOCIRecipeBuildAdmin(IBuildFarmJobAdmin):
@exported_as_webservice_entry(
- publish_web_link=True, as_of="devel", singular_name="oci_recipe_build")
-class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit,
- IOCIRecipeBuildView, IPackageBuild):
+ publish_web_link=True, as_of="devel", singular_name="oci_recipe_build"
+)
+class IOCIRecipeBuild(
+ IOCIRecipeBuildAdmin,
+ IOCIRecipeBuildEdit,
+ IOCIRecipeBuildView,
+ IPackageBuild,
+):
"""A build record for an OCI recipe."""
class IOCIRecipeBuildSet(ISpecificBuildFarmJobSource):
"""A utility to create and access OCIRecipeBuilds."""
- def new(requester, recipe, distro_arch_series,
- date_created=DEFAULT):
+ def new(requester, recipe, distro_arch_series, date_created=DEFAULT):
"""Create an `IOCIRecipeBuild`."""
def preloadBuildsData(builds):
@@ -318,23 +367,33 @@ class IOCIFile(Interface):
build = Reference(
IOCIRecipeBuild,
title=_("The OCI recipe build producing this file."),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
library_file = Reference(
- ILibraryFileAlias, title=_("A file in the librarian."),
- required=True, readonly=True)
+ ILibraryFileAlias,
+ title=_("A file in the librarian."),
+ required=True,
+ readonly=True,
+ )
layer_file_digest = TextLine(
- title=_("Content-addressable hash of the file''s contents, "
- "used for reassembling image layers when pushing "
- "a build to a registry. This hash is in an opaque format "
- "generated by the OCI build tool."),
- required=False, readonly=True)
+ title=_(
+ "Content-addressable hash of the file''s contents, "
+ "used for reassembling image layers when pushing "
+ "a build to a registry. This hash is in an opaque format "
+ "generated by the OCI build tool."
+ ),
+ required=False,
+ readonly=True,
+ )
date_last_used = Datetime(
title=_("The datetime this file was last used in a build."),
required=True,
- readonly=False)
+ readonly=False,
+ )
class IOCIFileSet(Interface):
diff --git a/lib/lp/oci/interfaces/ocirecipebuildjob.py b/lib/lp/oci/interfaces/ocirecipebuildjob.py
index df411e2..07f0ac6 100644
--- a/lib/lp/oci/interfaces/ocirecipebuildjob.py
+++ b/lib/lp/oci/interfaces/ocirecipebuildjob.py
@@ -4,40 +4,36 @@
"""OCIRecipe build job interfaces"""
__all__ = [
- 'IOCIRecipeBuildJob',
- 'IOCIRegistryUploadJob',
- 'IOCIRegistryUploadJobSource',
- ]
+ "IOCIRecipeBuildJob",
+ "IOCIRegistryUploadJob",
+ "IOCIRegistryUploadJobSource",
+]
from lazr.restful.fields import Reference
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Dict,
- List,
- TextLine,
- )
+from zope.interface import Attribute, Interface
+from zope.schema import Dict, List, TextLine
from lp import _
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
-from lp.services.job.interfaces.job import (
- IJob,
- IJobSource,
- IRunnableJob,
- )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
class IOCIRecipeBuildJob(Interface):
"""A job related to an OCI image."""
+
job = Reference(
- title=_("The common Job attributes."), schema=IJob,
- required=True, readonly=True)
+ title=_("The common Job attributes."),
+ schema=IJob,
+ required=True,
+ readonly=True,
+ )
build = Reference(
title=_("The OCI Recipe Build to use for this job."),
- schema=IOCIRecipeBuild, required=True, readonly=True)
+ schema=IOCIRecipeBuild,
+ required=True,
+ readonly=True,
+ )
json_data = Attribute(_("A dict of data about the job."))
@@ -46,20 +42,23 @@ class IOCIRegistryUploadJob(IRunnableJob):
"""A Job that uploads an OCI image to a registry."""
error_summary = TextLine(
- title=_("Error summary"), required=False, readonly=True)
+ title=_("Error summary"), required=False, readonly=True
+ )
errors = List(
title=_("Detailed registry upload errors"),
description=_(
"A list of errors, as described in "
"https://docs.docker.com/registry/spec/api/#errors, from the last "
- "attempt to run this job."),
+ "attempt to run this job."
+ ),
value_type=Dict(key_type=TextLine()),
- required=False, readonly=True)
+ required=False,
+ readonly=True,
+ )
class IOCIRegistryUploadJobSource(IJobSource):
-
def create(build):
"""Upload an OCI image to a registry.
diff --git a/lib/lp/oci/interfaces/ocirecipejob.py b/lib/lp/oci/interfaces/ocirecipejob.py
index a6e727d..a319673 100644
--- a/lib/lp/oci/interfaces/ocirecipejob.py
+++ b/lib/lp/oci/interfaces/ocirecipejob.py
@@ -4,48 +4,38 @@
"""Interfaces related to OCI recipe jobs."""
__all__ = [
- 'IOCIRecipeJob',
- 'IOCIRecipeRequestBuildsJob',
- 'IOCIRecipeRequestBuildsJobSource',
- ]
+ "IOCIRecipeJob",
+ "IOCIRecipeRequestBuildsJob",
+ "IOCIRecipeRequestBuildsJobSource",
+]
from lazr.restful.fields import Reference
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Datetime,
- Dict,
- Int,
- List,
- TextLine,
- )
+from zope.interface import Attribute, Interface
+from zope.schema import Datetime, Dict, Int, List, TextLine
from lp import _
-from lp.oci.interfaces.ocirecipe import (
- IOCIRecipe,
- IOCIRecipeBuildRequest,
- )
+from lp.oci.interfaces.ocirecipe import IOCIRecipe, IOCIRecipeBuildRequest
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.registry.interfaces.person import IPerson
-from lp.services.job.interfaces.job import (
- IJob,
- IJobSource,
- IRunnableJob,
- )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
class IOCIRecipeJob(Interface):
"""A job related to an OCI Recipe."""
job = Reference(
- title=_("The common Job attributes."), schema=IJob,
- required=True, readonly=True)
+ title=_("The common Job attributes."),
+ schema=IJob,
+ required=True,
+ readonly=True,
+ )
recipe = Reference(
title=_("The OCI recipe to use for this job."),
- schema=IOCIRecipe, required=True, readonly=True)
+ schema=IOCIRecipe,
+ required=True,
+ readonly=True,
+ )
metadata = Attribute(_("A dict of data about the job."))
@@ -54,33 +44,47 @@ class IOCIRecipeRequestBuildsJob(IRunnableJob):
"""A Job that processes a request for builds of an OCI recipe."""
requester = Reference(
- title=_("The person requesting the builds."), schema=IPerson,
- required=True, readonly=True)
+ title=_("The person requesting the builds."),
+ schema=IPerson,
+ required=True,
+ readonly=True,
+ )
build_request = Reference(
title=_("The build request corresponding to this job."),
- schema=IOCIRecipeBuildRequest, required=True, readonly=True)
+ schema=IOCIRecipeBuildRequest,
+ required=True,
+ readonly=True,
+ )
builds = List(
title=_("The builds created by this request."),
- value_type=Reference(schema=IOCIRecipeBuild), required=True,
- readonly=True)
+ value_type=Reference(schema=IOCIRecipeBuild),
+ required=True,
+ readonly=True,
+ )
date_created = Datetime(
title=_("Time when this job was created."),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
date_finished = Datetime(
- title=_("Time when this job finished."),
- required=True, readonly=True)
+ title=_("Time when this job finished."), required=True, readonly=True
+ )
error_message = TextLine(
- title=_("Error message"), required=False, readonly=True)
+ title=_("Error message"), required=False, readonly=True
+ )
uploaded_manifests = Dict(
title=_("A dict of manifest information per build."),
- key_type=Int(), value_type=Dict(),
- required=False, readonly=True)
+ key_type=Int(),
+ value_type=Dict(),
+ required=False,
+ readonly=True,
+ )
def addUploadedManifest(build_id, manifest_info):
"""Add the manifest information for one of the builds in this
@@ -92,7 +96,6 @@ class IOCIRecipeRequestBuildsJob(IRunnableJob):
class IOCIRecipeRequestBuildsJobSource(IJobSource):
-
def create(oci_recipe, requester, architectures=None):
"""Request builds of an OCI Recipe.
@@ -104,8 +107,7 @@ class IOCIRecipeRequestBuildsJobSource(IJobSource):
"""
def getByOCIRecipeAndID(recipe, job_id):
- """Retrieve the build job by OCI recipe and the given job ID.
- """
+ """Retrieve the build job by OCI recipe and the given job ID."""
def findByOCIRecipe(recipe, statuses=None, job_ids=None):
"""Find jobs for an OCI recipe.
diff --git a/lib/lp/oci/interfaces/ocirecipesubscription.py b/lib/lp/oci/interfaces/ocirecipesubscription.py
index b519c0e..3a63983 100644
--- a/lib/lp/oci/interfaces/ocirecipesubscription.py
+++ b/lib/lp/oci/interfaces/ocirecipesubscription.py
@@ -3,16 +3,11 @@
"""OCIRecipe subscription model."""
-__all__ = [
- 'IOCIRecipeSubscription'
-]
+__all__ = ["IOCIRecipeSubscription"]
from lazr.restful.fields import Reference
from zope.interface import Interface
-from zope.schema import (
- Datetime,
- Int,
- )
+from zope.schema import Datetime, Int
from lp import _
from lp.oci.interfaces.ocirecipe import IOCIRecipe
@@ -22,19 +17,27 @@ from lp.services.fields import PersonChoice
class IOCIRecipeSubscription(Interface):
"""A person subscription to a specific OCIRecipe recipe."""
- id = Int(title=_('ID'), readonly=True, required=True)
+ id = Int(title=_("ID"), readonly=True, required=True)
person = PersonChoice(
- title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',
+ title=_("Person"),
+ required=True,
+ vocabulary="ValidPersonOrTeam",
readonly=True,
- description=_("The person subscribed to the related OCI recipe."))
+ description=_("The person subscribed to the related OCI recipe."),
+ )
recipe = Reference(
- IOCIRecipe, title=_("OCI recipe"), required=True, readonly=True)
+ IOCIRecipe, title=_("OCI recipe"), required=True, readonly=True
+ )
subscribed_by = PersonChoice(
- title=_('Subscribed by'), required=True,
- vocabulary='ValidPersonOrTeam', readonly=True,
- description=_("The person who created this subscription."))
+ title=_("Subscribed by"),
+ required=True,
+ vocabulary="ValidPersonOrTeam",
+ readonly=True,
+ description=_("The person who created this subscription."),
+ )
date_created = Datetime(
- title=_('Date subscribed'), required=True, readonly=True)
+ title=_("Date subscribed"), required=True, readonly=True
+ )
def canBeUnsubscribedByUser(user):
"""Can the user unsubscribe the subscriber from the OCI recipe?"""
diff --git a/lib/lp/oci/interfaces/ociregistryclient.py b/lib/lp/oci/interfaces/ociregistryclient.py
index e149a28..f0241c2 100644
--- a/lib/lp/oci/interfaces/ociregistryclient.py
+++ b/lib/lp/oci/interfaces/ociregistryclient.py
@@ -4,11 +4,11 @@
"""Interface for communication with an OCI registry."""
__all__ = [
- 'BlobUploadFailed',
- 'IOCIRegistryClient',
- 'ManifestUploadFailed',
- 'MultipleOCIRegistryError',
- 'OCIRegistryError',
+ "BlobUploadFailed",
+ "IOCIRegistryClient",
+ "ManifestUploadFailed",
+ "MultipleOCIRegistryError",
+ "OCIRegistryError",
]
from zope.interface import Interface
@@ -31,8 +31,11 @@ class MultipleOCIRegistryError(OCIRegistryError):
@property
def errors(self):
- return [i.errors for i in self.exceptions
- if isinstance(i, OCIRegistryError)]
+ return [
+ i.errors
+ for i in self.exceptions
+ if isinstance(i, OCIRegistryError)
+ ]
class BlobUploadFailed(OCIRegistryError):
diff --git a/lib/lp/oci/interfaces/ociregistrycredentials.py b/lib/lp/oci/interfaces/ociregistrycredentials.py
index a10400c..53c0ea2 100644
--- a/lib/lp/oci/interfaces/ociregistrycredentials.py
+++ b/lib/lp/oci/interfaces/ociregistrycredentials.py
@@ -4,32 +4,23 @@
"""Interfaces for handling credentials for OCI registry actions."""
__all__ = [
- 'IOCIRegistryCredentials',
- 'IOCIRegistryCredentialsSet',
- 'OCIRegistryCredentialsAlreadyExist',
- 'OCIRegistryCredentialsNotOwner',
- 'user_can_edit_credentials_for_owner',
- ]
+ "IOCIRegistryCredentials",
+ "IOCIRegistryCredentialsSet",
+ "OCIRegistryCredentialsAlreadyExist",
+ "OCIRegistryCredentialsNotOwner",
+ "user_can_edit_credentials_for_owner",
+]
import http.client
from lazr.restful.declarations import error_status
from zope.interface import Interface
-from zope.schema import (
- Int,
- TextLine,
- )
+from zope.schema import Int, TextLine
from zope.security.interfaces import Unauthorized
from lp import _
-from lp.registry.interfaces.role import (
- IHasOwner,
- IPersonRoles,
- )
-from lp.services.fields import (
- PersonChoice,
- URIField,
- )
+from lp.registry.interfaces.role import IHasOwner, IPersonRoles
+from lp.services.fields import PersonChoice, URIField
@error_status(http.client.CONFLICT)
@@ -41,7 +32,8 @@ class OCIRegistryCredentialsAlreadyExist(Exception):
def __init__(self):
super().__init__(
"Credentials already exist with the same URL, username, and "
- "region.")
+ "region."
+ )
@error_status(http.client.UNAUTHORIZED)
@@ -60,13 +52,15 @@ class IOCIRegistryCredentialsView(Interface):
title=_("Username"),
description=_("The username for the credentials, if available."),
required=True,
- readonly=True)
+ readonly=True,
+ )
region = TextLine(
title=_("Region"),
description=_("The registry region, if available."),
required=False,
- readonly=True)
+ readonly=True,
+ )
class IOCIRegistryCredentialsEditableAttributes(IHasOwner):
@@ -75,17 +69,21 @@ class IOCIRegistryCredentialsEditableAttributes(IHasOwner):
title=_("Owner"),
required=True,
vocabulary="AllUserTeamsParticipationPlusSelf",
- description=_("The owner of these credentials. "
- "Only the owner is entitled to create "
- "push rules using them."),
- readonly=False)
+ description=_(
+ "The owner of these credentials. "
+ "Only the owner is entitled to create "
+ "push rules using them."
+ ),
+ readonly=False,
+ )
url = URIField(
- allowed_schemes=['http', 'https'],
+ allowed_schemes=["http", "https"],
title=_("URL"),
description=_("The registry URL."),
required=True,
- readonly=False)
+ readonly=False,
+ )
class IOCIRegistryCredentialsEdit(Interface):
@@ -100,9 +98,11 @@ class IOCIRegistryCredentialsEdit(Interface):
"""Delete these credentials."""
-class IOCIRegistryCredentials(IOCIRegistryCredentialsEdit,
- IOCIRegistryCredentialsEditableAttributes,
- IOCIRegistryCredentialsView):
+class IOCIRegistryCredentials(
+ IOCIRegistryCredentialsEdit,
+ IOCIRegistryCredentialsEditableAttributes,
+ IOCIRegistryCredentialsView,
+):
"""Credentials for pushing to an OCI registry."""
diff --git a/lib/lp/oci/interfaces/webservice.py b/lib/lp/oci/interfaces/webservice.py
index 9945259..01923a2 100644
--- a/lib/lp/oci/interfaces/webservice.py
+++ b/lib/lp/oci/interfaces/webservice.py
@@ -4,20 +4,20 @@
"""All the interfaces that are exposed through the webservice."""
__all__ = [
- 'IOCIProject',
- 'IOCIProjectSeries',
- 'IOCIPushRule',
- 'IOCIRecipe',
- 'IOCIRecipeBuild',
- 'IOCIRecipeBuildRequest'
- ]
+ "IOCIProject",
+ "IOCIProjectSeries",
+ "IOCIPushRule",
+ "IOCIRecipe",
+ "IOCIRecipeBuild",
+ "IOCIRecipeBuildRequest",
+]
from lp.oci.interfaces.ocipushrule import IOCIPushRule
from lp.oci.interfaces.ocirecipe import (
IOCIRecipe,
IOCIRecipeBuildRequest,
IOCIRecipeEdit,
- )
+)
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.ociprojectseries import IOCIProjectSeries
@@ -26,24 +26,24 @@ from lp.services.webservice.apihelpers import (
patch_entry_return_type,
patch_plain_parameter_type,
patch_reference_property,
- )
-
+)
# IOCIProject
-patch_collection_property(IOCIProject, 'series', IOCIProjectSeries)
-patch_entry_return_type(IOCIProject, 'newRecipe', IOCIRecipe)
+patch_collection_property(IOCIProject, "series", IOCIProjectSeries)
+patch_entry_return_type(IOCIProject, "newRecipe", IOCIRecipe)
patch_plain_parameter_type(
- IOCIProject, 'setOfficialRecipeStatus', 'recipe', IOCIRecipe)
+ IOCIProject, "setOfficialRecipeStatus", "recipe", IOCIRecipe
+)
# IOCIRecipe
-patch_collection_property(IOCIRecipe, 'builds', IOCIRecipeBuild)
-patch_collection_property(IOCIRecipe, 'completed_builds', IOCIRecipeBuild)
-patch_collection_property(IOCIRecipe, 'pending_builds', IOCIRecipeBuild)
-patch_collection_property(IOCIRecipe, 'push_rules', IOCIPushRule)
+patch_collection_property(IOCIRecipe, "builds", IOCIRecipeBuild)
+patch_collection_property(IOCIRecipe, "completed_builds", IOCIRecipeBuild)
+patch_collection_property(IOCIRecipe, "pending_builds", IOCIRecipeBuild)
+patch_collection_property(IOCIRecipe, "push_rules", IOCIPushRule)
# IOCIRecipeRequestBuild
-patch_reference_property(IOCIRecipeBuildRequest, 'recipe', IOCIRecipe)
-patch_collection_property(IOCIRecipeBuildRequest, 'builds', IOCIRecipeBuild)
+patch_reference_property(IOCIRecipeBuildRequest, "recipe", IOCIRecipe)
+patch_collection_property(IOCIRecipeBuildRequest, "builds", IOCIRecipeBuild)
-patch_entry_return_type(IOCIRecipeEdit, 'newPushRule', IOCIPushRule)
+patch_entry_return_type(IOCIRecipeEdit, "newPushRule", IOCIPushRule)
diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
index 872b2e5..483946a 100644
--- a/lib/lp/oci/model/ocipushrule.py
+++ b/lib/lp/oci/model/ocipushrule.py
@@ -4,41 +4,38 @@
"""Registry credentials for use by an `OCIPushRule`."""
__all__ = [
- 'OCIDistributionPushRule',
- 'OCIPushRule',
- 'OCIPushRuleSet',
- ]
-
-from storm.locals import (
- Int,
- Reference,
- Storm,
- Unicode,
- )
+ "OCIDistributionPushRule",
+ "OCIPushRule",
+ "OCIPushRuleSet",
+]
+
+from storm.locals import Int, Reference, Storm, Unicode
from zope.interface import implementer
from lp.oci.interfaces.ocipushrule import (
IOCIPushRule,
IOCIPushRuleSet,
OCIPushRuleAlreadyExists,
- )
+)
from lp.services.database.interfaces import IStore
@implementer(IOCIPushRule)
class OCIPushRule(Storm):
- __storm_table__ = 'OCIPushRule'
+ __storm_table__ = "OCIPushRule"
id = Int(primary=True)
- recipe_id = Int(name='recipe', allow_none=False)
- recipe = Reference(recipe_id, 'OCIRecipe.id')
+ recipe_id = Int(name="recipe", allow_none=False)
+ recipe = Reference(recipe_id, "OCIRecipe.id")
registry_credentials_id = Int(
- name='registry_credentials', allow_none=False)
+ name="registry_credentials", allow_none=False
+ )
registry_credentials = Reference(
- registry_credentials_id, 'OCIRegistryCredentials.id')
+ registry_credentials_id, "OCIRegistryCredentials.id"
+ )
image_name = Unicode(name="image_name", allow_none=False)
@@ -51,11 +48,15 @@ class OCIPushRule(Storm):
return self.registry_credentials.username
def setNewImageName(self, new_image_name):
- result = IStore(OCIPushRule).find(
- OCIPushRule,
- OCIPushRule.registry_credentials == self.registry_credentials,
- OCIPushRule.image_name == new_image_name
- ).one()
+ result = (
+ IStore(OCIPushRule)
+ .find(
+ OCIPushRule,
+ OCIPushRule.registry_credentials == self.registry_credentials,
+ OCIPushRule.image_name == new_image_name,
+ )
+ .one()
+ )
if result:
raise OCIPushRuleAlreadyExists()
self.image_name = new_image_name
@@ -93,13 +94,13 @@ class OCIDistributionPushRule:
@implementer(IOCIPushRuleSet)
class OCIPushRuleSet:
-
def new(self, recipe, registry_credentials, image_name):
"""See `IOCIPushRuleSet`."""
for existing in recipe.push_rules:
credentials_match = (
- existing.registry_credentials == registry_credentials)
- image_match = (existing.image_name == image_name)
+ existing.registry_credentials == registry_credentials
+ )
+ image_match = existing.image_name == image_name
if credentials_match and image_match:
raise OCIPushRuleAlreadyExists()
push_rule = OCIPushRule(recipe, registry_credentials, image_name)
@@ -112,12 +113,10 @@ class OCIPushRuleSet:
def findByRecipe(self, recipe):
store = IStore(OCIPushRule)
- return store.find(
- OCIPushRule,
- OCIPushRule.recipe == recipe)
+ return store.find(OCIPushRule, OCIPushRule.recipe == recipe)
def findByRegistryCredentials(self, credentials):
store = IStore(OCIPushRule)
return store.find(
- OCIPushRule,
- OCIPushRule.registry_credentials == credentials)
+ OCIPushRule, OCIPushRule.registry_credentials == credentials
+ )
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 73edd1a..53392f4 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -5,58 +5,31 @@
from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
-
__all__ = [
- 'get_ocirecipe_privacy_filter',
- 'OCIRecipe',
- 'OCIRecipeBuildRequest',
- 'OCIRecipeSet',
- ]
+ "get_ocirecipe_privacy_filter",
+ "OCIRecipe",
+ "OCIRecipeBuildRequest",
+ "OCIRecipeSet",
+]
-from lazr.lifecycle.event import ObjectCreatedEvent
import pytz
+from lazr.lifecycle.event import ObjectCreatedEvent
from storm.databases.postgres import JSON
-from storm.expr import (
- And,
- Coalesce,
- Desc,
- Exists,
- Join,
- Not,
- Or,
- Select,
- SQL,
- )
-from storm.locals import (
- Bool,
- DateTime,
- Int,
- Reference,
- Store,
- Storm,
- Unicode,
- )
-from zope.component import (
- getAdapter,
- getUtility,
- )
+from storm.expr import SQL, And, Coalesce, Desc, Exists, Join, Not, Or, Select
+from storm.locals import Bool, DateTime, Int, Reference, Store, Storm, Unicode
+from zope.component import getAdapter, getUtility
from zope.event import notify
from zope.interface import implementer
from zope.security.interfaces import Unauthorized
-from zope.security.proxy import (
- isinstance as zope_isinstance,
- removeSecurityProxy,
- )
+from zope.security.proxy import isinstance as zope_isinstance
+from zope.security.proxy import removeSecurityProxy
-from lp.app.enums import (
- InformationType,
- PUBLIC_INFORMATION_TYPES,
- )
+from lp.app.enums import PUBLIC_INFORMATION_TYPES, InformationType
from lp.app.errors import (
SubscriptionPrivacyViolation,
UserCannotUnsubscribePerson,
- )
+)
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.security import IAuthorization
from lp.app.interfaces.services import IService
@@ -72,6 +45,8 @@ from lp.code.model.gitrepository import GitRepository
from lp.oci.enums import OCIRecipeBuildRequestStatus
from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
from lp.oci.interfaces.ocirecipe import (
+ OCI_RECIPE_ALLOW_CREATE,
+ OCI_RECIPE_BUILD_DISTRIBUTION,
CannotModifyOCIRecipeProcessor,
DuplicateOCIRecipeName,
IOCIRecipe,
@@ -79,24 +54,17 @@ from lp.oci.interfaces.ocirecipe import (
IOCIRecipeSet,
NoSourceForOCIRecipe,
NoSuchOCIRecipe,
- OCI_RECIPE_ALLOW_CREATE,
- OCI_RECIPE_BUILD_DISTRIBUTION,
OCIRecipeBranchHasInvalidFormat,
OCIRecipeBuildAlreadyPending,
OCIRecipeFeatureDisabled,
OCIRecipeNotOwner,
OCIRecipePrivacyMismatch,
UsingDistributionCredentials,
- )
+)
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
-from lp.oci.interfaces.ociregistrycredentials import (
- IOCIRegistryCredentialsSet,
- )
-from lp.oci.model.ocipushrule import (
- OCIDistributionPushRule,
- OCIPushRule,
- )
+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentialsSet
+from lp.oci.model.ocipushrule import OCIDistributionPushRule, OCIPushRule
from lp.oci.model.ocirecipebuild import OCIRecipeBuild
from lp.oci.model.ocirecipejob import OCIRecipeJob
from lp.oci.model.ocirecipesubscription import OCIRecipeSubscription
@@ -104,49 +72,40 @@ from lp.registry.errors import PrivatePersonLinkageError
from lp.registry.interfaces.accesspolicy import (
IAccessArtifactGrantSource,
IAccessArtifactSource,
- )
+)
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import (
IPerson,
IPersonSet,
validate_public_person,
- )
+)
from lp.registry.interfaces.role import IPersonRoles
from lp.registry.model.accesspolicy import (
AccessPolicyGrant,
reconcile_access_for_artifacts,
- )
+)
from lp.registry.model.distribution import Distribution
from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.person import Person
from lp.registry.model.series import ACTIVE_STATUSES
from lp.registry.model.teammembership import TeamParticipation
from lp.services.database.bulk import load_related
-from lp.services.database.constants import (
- DEFAULT,
- UTC_NOW,
- )
+from lp.services.database.constants import DEFAULT, UTC_NOW
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormexpr import (
Array,
ArrayAgg,
ArrayIntersects,
Greatest,
NullsLast,
- )
+)
from lp.services.features import getFeatureFlag
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -164,15 +123,17 @@ def oci_recipe_modified(recipe, event):
@implementer(IOCIRecipe)
class OCIRecipe(Storm, WebhookTargetMixin):
- __storm_table__ = 'OCIRecipe'
+ __storm_table__ = "OCIRecipe"
id = Int(primary=True)
date_created = DateTime(
- name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ name="date_created", tzinfo=pytz.UTC, allow_none=False
+ )
date_last_modified = DateTime(
- name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
+ name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+ )
- registrant_id = Int(name='registrant', allow_none=False)
+ registrant_id = Int(name="registrant", allow_none=False)
registrant = Reference(registrant_id, "Person.id")
def _validate_owner(self, attr, value):
@@ -181,11 +142,12 @@ class OCIRecipe(Storm, WebhookTargetMixin):
validate_public_person(self, attr, value)
except PrivatePersonLinkageError:
raise OCIRecipePrivacyMismatch(
- "A public OCI recipe cannot have a private owner.")
+ "A public OCI recipe cannot have a private owner."
+ )
return value
- owner_id = Int(name='owner', allow_none=False, validator=_validate_owner)
- owner = Reference(owner_id, 'Person.id')
+ owner_id = Int(name="owner", allow_none=False, validator=_validate_owner)
+ owner = Reference(owner_id, "Person.id")
def _valid_information_type(self, attr, value):
if value not in PUBLIC_INFORMATION_TYPES:
@@ -199,11 +161,13 @@ class OCIRecipe(Storm, WebhookTargetMixin):
return value
_information_type = DBEnum(
- enum=InformationType, default=InformationType.PUBLIC,
+ enum=InformationType,
+ default=InformationType.PUBLIC,
name="information_type",
- validator=_valid_information_type)
+ validator=_valid_information_type,
+ )
- oci_project_id = Int(name='oci_project', allow_none=False)
+ oci_project_id = Int(name="oci_project", allow_none=False)
oci_project = Reference(oci_project_id, "OCIProject.id")
name = Unicode(name="name", allow_none=False)
@@ -217,32 +181,50 @@ class OCIRecipe(Storm, WebhookTargetMixin):
if not self.private and value is not None:
if IStore(GitRepository).get(GitRepository, value).private:
raise OCIRecipePrivacyMismatch(
- "A public OCI recipe cannot have a private repository.")
+ "A public OCI recipe cannot have a private repository."
+ )
return value
git_repository_id = Int(
- name="git_repository", allow_none=True,
- validator=_validate_git_repository)
+ name="git_repository",
+ allow_none=True,
+ validator=_validate_git_repository,
+ )
git_repository = Reference(git_repository_id, "GitRepository.id")
git_path = Unicode(name="git_path", allow_none=True)
build_file = Unicode(name="build_file", allow_none=False)
build_path = Unicode(name="build_path", allow_none=False)
_build_args = JSON(name="build_args", allow_none=True)
- require_virtualized = Bool(name="require_virtualized", default=True,
- allow_none=False)
+ require_virtualized = Bool(
+ name="require_virtualized", default=True, allow_none=False
+ )
- allow_internet = Bool(name='allow_internet', allow_none=False)
+ allow_internet = Bool(name="allow_internet", allow_none=False)
build_daily = Bool(name="build_daily", default=False)
_image_name = Unicode(name="image_name", allow_none=True)
- def __init__(self, name, registrant, owner, oci_project, git_ref,
- description=None, official=False, require_virtualized=True,
- build_file=None, build_daily=False, date_created=DEFAULT,
- allow_internet=True, build_args=None, build_path=None,
- image_name=None, information_type=InformationType.PUBLIC):
+ def __init__(
+ self,
+ name,
+ registrant,
+ owner,
+ oci_project,
+ git_ref,
+ description=None,
+ official=False,
+ require_virtualized=True,
+ build_file=None,
+ build_daily=False,
+ date_created=DEFAULT,
+ allow_internet=True,
+ build_args=None,
+ build_path=None,
+ image_name=None,
+ information_type=InformationType.PUBLIC,
+ ):
if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
raise OCIRecipeFeatureDisabled()
super().__init__()
@@ -266,8 +248,11 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def __repr__(self):
return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % (
- self.owner.name, self.oci_project.pillar.name,
- self.oci_project.name, self.name)
+ self.owner.name,
+ self.oci_project.pillar.name,
+ self.oci_project.name,
+ self.name,
+ )
@property
def information_type(self):
@@ -318,8 +303,9 @@ class OCIRecipe(Storm, WebhookTargetMixin):
Takes the privacy and pillar and makes the related AccessArtifact
and AccessPolicyArtifacts match.
"""
- reconcile_access_for_artifacts([self], self.information_type,
- [self.pillar])
+ reconcile_access_for_artifacts(
+ [self], self.information_type, [self.pillar]
+ )
def getAllowedInformationTypes(self, user):
"""See `IOCIRecipe`."""
@@ -335,70 +321,85 @@ class OCIRecipe(Storm, WebhookTargetMixin):
return not store.find(
OCIRecipe,
OCIRecipe.id == self.id,
- get_ocirecipe_privacy_filter(user)).is_empty()
+ get_ocirecipe_privacy_filter(user),
+ ).is_empty()
def getSubscription(self, person):
"""See `IOCIRecipe`."""
if person is None:
return None
- return Store.of(self).find(
- OCIRecipeSubscription,
- OCIRecipeSubscription.person == person,
- OCIRecipeSubscription.recipe == self).one()
+ return (
+ Store.of(self)
+ .find(
+ OCIRecipeSubscription,
+ OCIRecipeSubscription.person == person,
+ OCIRecipeSubscription.recipe == self,
+ )
+ .one()
+ )
def userCanBeSubscribed(self, person):
"""Checks if the given person can subscribe to this OCI recipe."""
return not (
- self.private and
- person.is_team and
- person.anyone_can_join())
+ self.private and person.is_team and person.anyone_can_join()
+ )
@property
def subscriptions(self):
return Store.of(self).find(
- OCIRecipeSubscription,
- OCIRecipeSubscription.recipe == self)
+ OCIRecipeSubscription, OCIRecipeSubscription.recipe == self
+ )
@property
def subscribers(self):
return Store.of(self).find(
Person,
OCIRecipeSubscription.person_id == Person.id,
- OCIRecipeSubscription.recipe == self)
+ OCIRecipeSubscription.recipe == self,
+ )
def subscribe(self, person, subscribed_by, ignore_permissions=False):
"""See `IOCIRecipe`."""
if not self.userCanBeSubscribed(person):
raise SubscriptionPrivacyViolation(
"Open and delegated teams cannot be subscribed to private "
- "OCI recipes.")
+ "OCI recipes."
+ )
subscription = self.getSubscription(person)
if subscription is None:
subscription = OCIRecipeSubscription(
- person=person, recipe=self, subscribed_by=subscribed_by)
+ person=person, recipe=self, subscribed_by=subscribed_by
+ )
Store.of(subscription).flush()
service = getUtility(IService, "sharing")
ocirecipes = service.getVisibleArtifacts(
- person, ocirecipes=[self], ignore_permissions=True)["ocirecipes"]
+ person, ocirecipes=[self], ignore_permissions=True
+ )["ocirecipes"]
if not ocirecipes:
service.ensureAccessGrants(
- [person], subscribed_by, ocirecipes=[self],
- ignore_permissions=ignore_permissions)
+ [person],
+ subscribed_by,
+ ocirecipes=[self],
+ ignore_permissions=ignore_permissions,
+ )
def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
"""See `IOCIRecipe`."""
subscription = self.getSubscription(person)
if subscription is None:
return
- if (not ignore_permissions
- and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
+ if (
+ not ignore_permissions
+ and not subscription.canBeUnsubscribedByUser(unsubscribed_by)
+ ):
raise UserCannotUnsubscribePerson(
- '%s does not have permission to unsubscribe %s.' % (
- unsubscribed_by.displayname,
- person.displayname))
+ "%s does not have permission to unsubscribe %s."
+ % (unsubscribed_by.displayname, person.displayname)
+ )
artifact = getUtility(IAccessArtifactSource).find([self])
getUtility(IAccessArtifactGrantSource).revokeByArtifact(
- artifact, [person])
+ artifact, [person]
+ )
store = Store.of(subscription)
store.remove(subscription)
IStore(self).flush()
@@ -409,7 +410,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def _deleteOCIRecipeSubscriptions(self):
subscriptions = Store.of(self).find(
- OCIRecipeSubscription, OCIRecipeSubscription.recipe == self)
+ OCIRecipeSubscription, OCIRecipeSubscription.recipe == self
+ )
subscriptions.remove()
def destroySelf(self):
@@ -423,30 +425,41 @@ class OCIRecipe(Storm, WebhookTargetMixin):
buildqueue_records = store.find(
BuildQueue,
BuildQueue._build_farm_job_id == OCIRecipeBuild.build_farm_job_id,
- OCIRecipeBuild.recipe == self)
+ OCIRecipeBuild.recipe == self,
+ )
for buildqueue_record in buildqueue_records:
buildqueue_record.destroySelf()
- build_farm_job_ids = list(store.find(
- OCIRecipeBuild.build_farm_job_id, OCIRecipeBuild.recipe == self))
-
- store.execute("""
+ build_farm_job_ids = list(
+ store.find(
+ OCIRecipeBuild.build_farm_job_id, OCIRecipeBuild.recipe == self
+ )
+ )
+
+ store.execute(
+ """
DELETE FROM OCIFile
USING OCIRecipeBuild
WHERE
OCIFile.build = OCIRecipeBuild.id AND
OCIRecipeBuild.recipe = ?
- """, (self.id,))
- store.execute("""
+ """,
+ (self.id,),
+ )
+ store.execute(
+ """
DELETE FROM OCIRecipeBuildJob
USING OCIRecipeBuild
WHERE
OCIRecipeBuildJob.build = OCIRecipeBuild.id AND
OCIRecipeBuild.recipe = ?
- """, (self.id,))
+ """,
+ (self.id,),
+ )
affected_jobs = Select(
[OCIRecipeJob.job_id],
- And(OCIRecipeJob.job == Job.id, OCIRecipeJob.recipe == self))
+ And(OCIRecipeJob.job == Job.id, OCIRecipeJob.recipe == self),
+ )
builds = store.find(OCIRecipeBuild, OCIRecipeBuild.recipe == self)
builds.remove()
store.find(Job, Job.id.is_in(affected_jobs)).remove()
@@ -455,7 +468,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
getUtility(IWebhookSet).delete(self.webhooks)
store.remove(self)
store.find(
- BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
+ BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)
+ ).remove()
@cachedproperty
def _git_ref(self):
@@ -492,8 +506,9 @@ class OCIRecipe(Storm, WebhookTargetMixin):
distro = getUtility(IDistributionSet).getByName(distro_name)
if not distro:
raise ValueError(
- "'%s' is not a valid value for feature flag '%s'" %
- (distro_name, OCI_RECIPE_BUILD_DISTRIBUTION))
+ "'%s' is not a valid value for feature flag '%s'"
+ % (distro_name, OCI_RECIPE_BUILD_DISTRIBUTION)
+ )
return distro
@property
@@ -501,8 +516,9 @@ class OCIRecipe(Storm, WebhookTargetMixin):
# For OCI builds we default to the series set by the feature flag.
# If the feature flag is not set we default to the current series of
# the recipe's distribution.
- oci_series = getFeatureFlag('oci.build_series.%s'
- % self.distribution.name)
+ oci_series = getFeatureFlag(
+ "oci.build_series.%s" % self.distribution.name
+ )
if oci_series:
return self.distribution.getSeries(oci_series)
else:
@@ -514,26 +530,37 @@ class OCIRecipe(Storm, WebhookTargetMixin):
clauses = [Processor.id == DistroArchSeries.processor_id]
if self.distro_series is not None:
enabled_archs_resultset = removeSecurityProxy(
- self.distro_series.enabled_architectures)
- clauses.append(DistroArchSeries.id.is_in(
- enabled_archs_resultset.get_select_expr(DistroArchSeries.id)))
+ self.distro_series.enabled_architectures
+ )
+ clauses.append(
+ DistroArchSeries.id.is_in(
+ enabled_archs_resultset.get_select_expr(
+ DistroArchSeries.id
+ )
+ )
+ )
else:
# We might not know the series if the OCI project's distribution
# has no series at all, which can happen in tests. Fall back to
# just returning enabled architectures for any active series,
# which is a bit of a hack but works.
- clauses.extend([
- DistroArchSeries.enabled,
- DistroArchSeries.distroseriesID == DistroSeries.id,
- DistroSeries.status.is_in(ACTIVE_STATUSES),
- ])
+ clauses.extend(
+ [
+ DistroArchSeries.enabled,
+ DistroArchSeries.distroseriesID == DistroSeries.id,
+ DistroSeries.status.is_in(ACTIVE_STATUSES),
+ ]
+ )
return Store.of(self).find(Processor, *clauses).config(distinct=True)
def _getProcessors(self):
- return list(Store.of(self).find(
- Processor,
- Processor.id == OCIRecipeArch.processor_id,
- OCIRecipeArch.recipe == self))
+ return list(
+ Store.of(self).find(
+ Processor,
+ Processor.id == OCIRecipeArch.processor_id,
+ OCIRecipeArch.recipe == self,
+ )
+ )
def setProcessors(self, processors, check_permissions=False, user=None):
"""See `IOCIRecipe`."""
@@ -542,21 +569,25 @@ class OCIRecipe(Storm, WebhookTargetMixin):
if user is not None:
roles = IPersonRoles(user)
authz = lambda perm: getAdapter(self, IAuthorization, perm)
- if authz('launchpad.Admin').checkAuthenticated(roles):
+ if authz("launchpad.Admin").checkAuthenticated(roles):
can_modify = lambda proc: True
- elif authz('launchpad.Edit').checkAuthenticated(roles):
+ elif authz("launchpad.Edit").checkAuthenticated(roles):
can_modify = lambda proc: not proc.restricted
if can_modify is None:
raise Unauthorized(
- 'Permission launchpad.Admin or launchpad.Edit required '
- 'on %s.' % self)
+ "Permission launchpad.Admin or launchpad.Edit required "
+ "on %s." % self
+ )
else:
can_modify = lambda proc: True
- enablements = dict(Store.of(self).find(
- (Processor, OCIRecipeArch),
- Processor.id == OCIRecipeArch.processor_id,
- OCIRecipeArch.recipe == self))
+ enablements = dict(
+ Store.of(self).find(
+ (Processor, OCIRecipeArch),
+ Processor.id == OCIRecipeArch.processor_id,
+ OCIRecipeArch.recipe == self,
+ )
+ )
for proc in enablements:
if proc not in processors:
if not can_modify(proc):
@@ -582,26 +613,31 @@ class OCIRecipe(Storm, WebhookTargetMixin):
and das.processor in self.processors
and (
das.processor.supports_virtualized
- or not self.require_virtualized))
+ or not self.require_virtualized
+ )
+ )
def _isArchitectureAllowed(self, das, pocket):
- return (
- das.getChroot(pocket=pocket) is not None
- and self._isBuildableArchitectureAllowed(das))
+ return das.getChroot(
+ pocket=pocket
+ ) is not None and self._isBuildableArchitectureAllowed(das)
def getAllowedArchitectures(self, distro_series=None):
"""See `IOCIRecipe`."""
if distro_series is None:
distro_series = self.distro_series
return [
- das for das in distro_series.buildable_architectures
- if self._isBuildableArchitectureAllowed(das)]
+ das
+ for das in distro_series.buildable_architectures
+ if self._isBuildableArchitectureAllowed(das)
+ ]
def _checkRequestBuild(self, requester):
if not requester.inTeam(self.owner):
raise OCIRecipeNotOwner(
- "%s cannot create OCI image builds owned by %s." %
- (requester.display_name, self.owner.display_name))
+ "%s cannot create OCI image builds owned by %s."
+ % (requester.display_name, self.owner.display_name)
+ )
def _hasPendingBuilds(self, distro_arch_series):
"""Checks if this OCIRecipe has pending builds for all processors
@@ -615,7 +651,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
OCIRecipeBuild,
OCIRecipeBuild.recipe == self.id,
OCIRecipeBuild.processor_id.is_in([p.id for p in processors]),
- OCIRecipeBuild.status == BuildStatus.NEEDSBUILD)
+ OCIRecipeBuild.status == BuildStatus.NEEDSBUILD,
+ )
pending_processors = {i.processor for i in pending}
return len(pending_processors) == len(processors)
@@ -625,7 +662,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
raise OCIRecipeBuildAlreadyPending
build = getUtility(IOCIRecipeBuildSet).new(
- requester, self, distro_arch_series, build_request=build_request)
+ requester, self, distro_arch_series, build_request=build_request
+ )
build.queueBuild()
notify(ObjectCreatedEvent(build, user=requester))
return build
@@ -633,19 +671,25 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def getBuildRequest(self, job_id):
return OCIRecipeBuildRequest(self, job_id)
- def requestBuildsFromJob(self, requester, build_request=None,
- architectures=None):
+ def requestBuildsFromJob(
+ self, requester, build_request=None, architectures=None
+ ):
self._checkRequestBuild(requester)
distro_arch_series = set(self.getAllowedArchitectures())
builds = []
for das in distro_arch_series:
- if (architectures is not None
- and das.architecturetag not in architectures):
+ if (
+ architectures is not None
+ and das.architecturetag not in architectures
+ ):
continue
try:
- builds.append(self.requestBuild(
- requester, das, build_request=build_request))
+ builds.append(
+ self.requestBuild(
+ requester, das, build_request=build_request
+ )
+ )
except OCIRecipeBuildAlreadyPending:
pass
@@ -659,14 +703,16 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def requestBuilds(self, requester, architectures=None):
self._checkRequestBuild(requester)
job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
- self, requester, architectures)
+ self, requester, architectures
+ )
return self.getBuildRequest(job.job_id)
@property
def pending_build_requests(self):
util = getUtility(IOCIRecipeRequestBuildsJobSource)
return util.findByOCIRecipe(
- self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
+ self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)
+ )
@property
def push_rules(self):
@@ -676,11 +722,10 @@ class OCIRecipe(Storm, WebhookTargetMixin):
push_rule = OCIDistributionPushRule(
self,
self.oci_project.distribution.oci_registry_credentials,
- self.image_name)
+ self.image_name,
+ )
return [push_rule]
- rules = IStore(self).find(
- OCIPushRule,
- OCIPushRule.recipe == self.id)
+ rules = IStore(self).find(OCIPushRule, OCIPushRule.recipe == self.id)
return list(rules)
@property
@@ -691,13 +736,13 @@ class OCIRecipe(Storm, WebhookTargetMixin):
BuildStatus.BUILDING,
BuildStatus.UPLOADING,
BuildStatus.CANCELLING,
- ]
+ ]
def _getBuilds(self, filter_term, order_by):
"""The actual query to get the builds."""
query_args = [
OCIRecipeBuild.recipe == self,
- ]
+ ]
if filter_term is not None:
query_args.append(filter_term)
result = Store.of(self).find(OCIRecipeBuild, *query_args)
@@ -713,22 +758,34 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def builds(self):
"""See `IOCIRecipe`."""
order_by = (
- NullsLast(Desc(Greatest(
- OCIRecipeBuild.date_started,
- OCIRecipeBuild.date_finished))),
+ NullsLast(
+ Desc(
+ Greatest(
+ OCIRecipeBuild.date_started,
+ OCIRecipeBuild.date_finished,
+ )
+ )
+ ),
Desc(OCIRecipeBuild.date_created),
- Desc(OCIRecipeBuild.id))
+ Desc(OCIRecipeBuild.id),
+ )
return self._getBuilds(None, order_by)
@property
def completed_builds(self):
"""See `IOCIRecipe`."""
- filter_term = (Not(OCIRecipeBuild.status.is_in(self._pending_states)))
+ filter_term = Not(OCIRecipeBuild.status.is_in(self._pending_states))
order_by = (
- NullsLast(Desc(Greatest(
- OCIRecipeBuild.date_started,
- OCIRecipeBuild.date_finished))),
- Desc(OCIRecipeBuild.id))
+ NullsLast(
+ Desc(
+ Greatest(
+ OCIRecipeBuild.date_started,
+ OCIRecipeBuild.date_finished,
+ )
+ )
+ ),
+ Desc(OCIRecipeBuild.id),
+ )
return self._getBuilds(filter_term, order_by)
@property
@@ -736,19 +793,25 @@ class OCIRecipe(Storm, WebhookTargetMixin):
"""See `IOCIRecipe`."""
filter_term = (
Not(OCIRecipeBuild.status.is_in(self._pending_states)),
- OCIRecipeBuild.build_request_id == None)
+ OCIRecipeBuild.build_request_id == None,
+ )
order_by = (
- NullsLast(Desc(Greatest(
- OCIRecipeBuild.date_started,
- OCIRecipeBuild.date_finished))),
- Desc(OCIRecipeBuild.id))
+ NullsLast(
+ Desc(
+ Greatest(
+ OCIRecipeBuild.date_started,
+ OCIRecipeBuild.date_finished,
+ )
+ )
+ ),
+ Desc(OCIRecipeBuild.id),
+ )
return self._getBuilds(filter_term, order_by)
-
@property
def pending_builds(self):
"""See `IOCIRecipe`."""
- filter_term = (OCIRecipeBuild.status.is_in(self._pending_states))
+ filter_term = OCIRecipeBuild.status.is_in(self._pending_states)
# We want to order by date_created but this is the same as ordering
# by id (since id increases monotonically) and is less expensive.
order_by = Desc(OCIRecipeBuild.id)
@@ -776,8 +839,14 @@ class OCIRecipe(Storm, WebhookTargetMixin):
def image_name(self, value):
self._image_name = value
- def newPushRule(self, registrant, registry_url, image_name, credentials,
- credentials_owner=None):
+ def newPushRule(
+ self,
+ registrant,
+ registry_url,
+ image_name,
+ credentials,
+ credentials_owner=None,
+ ):
"""See `IOCIRecipe`."""
if credentials_owner is None:
# Ideally this should probably be a required parameter, but
@@ -788,9 +857,11 @@ class OCIRecipe(Storm, WebhookTargetMixin):
if self.use_distribution_credentials:
raise UsingDistributionCredentials()
oci_credentials = getUtility(IOCIRegistryCredentialsSet).getOrCreate(
- registrant, credentials_owner, registry_url, credentials)
+ registrant, credentials_owner, registry_url, credentials
+ )
push_rule = getUtility(IOCIPushRuleSet).new(
- self, oci_credentials, image_name)
+ self, oci_credentials, image_name
+ )
Store.of(push_rule).flush()
return push_rule
@@ -814,22 +885,38 @@ class OCIRecipeArch(Storm):
@implementer(IOCIRecipeSet)
class OCIRecipeSet:
-
- def new(self, name, registrant, owner, oci_project, git_ref, build_file,
- description=None, official=False, require_virtualized=True,
- build_daily=False, processors=None, date_created=DEFAULT,
- allow_internet=True, build_args=None, build_path=None,
- image_name=None, information_type=InformationType.PUBLIC):
+ def new(
+ self,
+ name,
+ registrant,
+ owner,
+ oci_project,
+ git_ref,
+ build_file,
+ description=None,
+ official=False,
+ require_virtualized=True,
+ build_daily=False,
+ processors=None,
+ date_created=DEFAULT,
+ allow_internet=True,
+ build_args=None,
+ build_path=None,
+ image_name=None,
+ information_type=InformationType.PUBLIC,
+ ):
"""See `IOCIRecipeSet`."""
if not registrant.inTeam(owner):
if owner.is_team:
raise OCIRecipeNotOwner(
- "%s is not a member of %s." %
- (registrant.displayname, owner.displayname))
+ "%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))
+ "%s cannot create OCI images owned by %s."
+ % (registrant.displayname, owner.displayname)
+ )
if not (git_ref and build_file):
raise NoSourceForOCIRecipe
@@ -845,31 +932,52 @@ class OCIRecipeSet:
store = IMasterStore(OCIRecipe)
oci_recipe = OCIRecipe(
- name, registrant, owner, oci_project, git_ref, description,
- official, require_virtualized, build_file, build_daily,
- date_created, allow_internet, build_args, build_path, image_name,
- information_type)
+ name,
+ registrant,
+ owner,
+ oci_project,
+ git_ref,
+ description,
+ official,
+ require_virtualized,
+ build_file,
+ build_daily,
+ date_created,
+ allow_internet,
+ build_args,
+ build_path,
+ image_name,
+ information_type,
+ )
store.add(oci_recipe)
oci_recipe._reconcileAccess()
# Automatically subscribe the owner to the OCI recipe.
- oci_recipe.subscribe(oci_recipe.owner, registrant,
- ignore_permissions=True)
+ oci_recipe.subscribe(
+ oci_recipe.owner, registrant, ignore_permissions=True
+ )
if processors is None:
processors = [
- p for p in oci_recipe.available_processors
- if p.build_by_default]
+ p
+ for p in oci_recipe.available_processors
+ if p.build_by_default
+ ]
oci_recipe.setProcessors(processors)
return oci_recipe
def _getByName(self, owner, oci_project, name):
- return IStore(OCIRecipe).find(
- OCIRecipe,
- OCIRecipe.owner == owner,
- OCIRecipe.name == name,
- OCIRecipe.oci_project == oci_project).one()
+ return (
+ IStore(OCIRecipe)
+ .find(
+ OCIRecipe,
+ OCIRecipe.owner == owner,
+ OCIRecipe.name == name,
+ OCIRecipe.oci_project == oci_project,
+ )
+ .one()
+ )
def exists(self, owner, oci_project, name):
"""See `IOCIRecipeSet`."""
@@ -898,17 +1006,20 @@ class OCIRecipeSet:
return IStore(OCIRecipe).find(
OCIRecipe,
OCIRecipe.oci_project == oci_project,
- get_ocirecipe_privacy_filter(visible_by_user))
+ get_ocirecipe_privacy_filter(visible_by_user),
+ )
def findByContext(self, context, visible_by_user):
if IPerson.providedBy(context):
return self.findByOwner(context).find(
- get_ocirecipe_privacy_filter(visible_by_user))
+ get_ocirecipe_privacy_filter(visible_by_user)
+ )
elif IOCIProject.providedBy(context):
return self.findByOCIProject(context, visible_by_user)
else:
raise NotImplementedError(
- "Unknown OCI recipe context: %s" % context)
+ "Unknown OCI recipe context: %s" % context
+ )
def findByGitRepository(self, repository, paths=None):
"""See `IOCIRecipeSet`."""
@@ -920,7 +1031,8 @@ class OCIRecipeSet:
def detachFromGitRepository(self, repository):
"""See `IOCIRecipeSet`."""
self.findByGitRepository(repository).set(
- git_repository_id=None, git_path=None, date_last_modified=UTC_NOW)
+ git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
+ )
def preloadDataForOCIRecipes(self, recipes, user=None):
"""See `IOCIRecipeSet`."""
@@ -931,8 +1043,11 @@ class OCIRecipeSet:
person_ids.add(recipe.registrant_id)
person_ids.add(recipe.owner_id)
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- person_ids, need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True
+ )
+ )
# Preload projects
oci_projects = [recipe.oci_project for recipe in recipes]
@@ -940,12 +1055,13 @@ class OCIRecipeSet:
# Preload repos
repos = load_related(GitRepository, recipes, ["git_repository_id"])
- load_related(Person, repos, ['owner_id', 'registrant_id'])
+ load_related(Person, repos, ["owner_id", "registrant_id"])
GenericGitCollection.preloadDataForRepositories(repos)
# Preload GitRefs.
git_refs = GitRef.findByReposAndPaths(
- [(r.git_repository, r.git_path) for r in recipes])
+ [(r.git_repository, r.git_path) for r in recipes]
+ )
for recipe in recipes:
git_ref = git_refs.get((recipe.git_repository, recipe.git_path))
if git_ref is not None:
@@ -957,45 +1073,49 @@ class OCIRecipeSet:
def collect_builds(*states):
wanted = []
for state in states:
- candidates = [build for build in builds
- if build.status == state]
+ candidates = [
+ build for build in builds if build.status == state
+ ]
wanted.extend(candidates)
return wanted
- failed = collect_builds(BuildStatus.FAILEDTOBUILD,
- BuildStatus.MANUALDEPWAIT,
- BuildStatus.CHROOTWAIT,
- BuildStatus.FAILEDTOUPLOAD)
+
+ failed = collect_builds(
+ BuildStatus.FAILEDTOBUILD,
+ BuildStatus.MANUALDEPWAIT,
+ BuildStatus.CHROOTWAIT,
+ BuildStatus.FAILEDTOUPLOAD,
+ )
needsbuild = collect_builds(BuildStatus.NEEDSBUILD)
- building = collect_builds(BuildStatus.BUILDING,
- BuildStatus.UPLOADING)
+ building = collect_builds(BuildStatus.BUILDING, BuildStatus.UPLOADING)
successful = collect_builds(BuildStatus.FULLYBUILT)
- cancelled = collect_builds(BuildStatus.CANCELLING,
- BuildStatus.CANCELLED)
+ cancelled = collect_builds(
+ BuildStatus.CANCELLING, BuildStatus.CANCELLED
+ )
# Note: the BuildStatus DBItems are used here to summarize the
# status of a set of builds:s
if len(building) != 0:
return {
- 'status': BuildSetStatus.BUILDING,
- 'builds': building,
- }
+ "status": BuildSetStatus.BUILDING,
+ "builds": building,
+ }
# If there are no builds, this is a 'pending build request'
# and needs building
elif len(needsbuild) != 0 or len(builds) == 0:
return {
- 'status': BuildSetStatus.NEEDSBUILD,
- 'builds': needsbuild,
- }
+ "status": BuildSetStatus.NEEDSBUILD,
+ "builds": needsbuild,
+ }
elif len(failed) != 0 or len(cancelled) != 0:
return {
- 'status': BuildSetStatus.FAILEDTOBUILD,
- 'builds': failed,
- }
+ "status": BuildSetStatus.FAILEDTOBUILD,
+ "builds": failed,
+ }
else:
return {
- 'status': BuildSetStatus.FULLYBUILT,
- 'builds': successful,
- }
+ "status": BuildSetStatus.FULLYBUILT,
+ "builds": successful,
+ }
@implementer(IOCIRecipeBuildRequest)
@@ -1005,6 +1125,7 @@ class OCIRecipeBuildRequest:
This is not directly backed by a database table; instead, it is a
webservice-friendly view of an asynchronous build request.
"""
+
def __init__(self, oci_recipe, id):
self.recipe = oci_recipe
self.id = id
@@ -1012,8 +1133,7 @@ class OCIRecipeBuildRequest:
@cachedproperty
def job(self):
util = getUtility(IOCIRecipeRequestBuildsJobSource)
- return util.getByOCIRecipeAndID(
- self.recipe, self.id)
+ return util.getByOCIRecipeAndID(self.recipe, self.id)
@property
def date_requested(self):
@@ -1070,7 +1190,8 @@ def get_ocirecipe_privacy_filter(user):
# information_type, we should be able to change this.
private_recipe = SQL(
"COALESCE(information_type NOT IN ?, false)",
- params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)])
+ params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)],
+ )
if user is None:
return private_recipe == False
@@ -1080,28 +1201,45 @@ def get_ocirecipe_privacy_filter(user):
Select(
ArrayAgg(TeamParticipation.teamID),
tables=TeamParticipation,
- where=(TeamParticipation.person == user)
- )), False)
+ where=(TeamParticipation.person == user),
+ ),
+ ),
+ False,
+ )
policy_grant_query = Coalesce(
ArrayIntersects(
Array(SQL("%s.access_policy" % OCIRecipe.__storm_table__)),
Select(
ArrayAgg(AccessPolicyGrant.policy_id),
- tables=(AccessPolicyGrant,
- Join(TeamParticipation,
- TeamParticipation.teamID ==
- AccessPolicyGrant.grantee_id)),
- where=(TeamParticipation.person == user)
- )), False)
+ tables=(
+ AccessPolicyGrant,
+ Join(
+ TeamParticipation,
+ TeamParticipation.teamID
+ == AccessPolicyGrant.grantee_id,
+ ),
+ ),
+ where=(TeamParticipation.person == user),
+ ),
+ ),
+ False,
+ )
admin_team_id = getUtility(ILaunchpadCelebrities).admin.id
- user_is_admin = Exists(Select(
- TeamParticipation.personID,
- tables=[TeamParticipation],
- where=And(
- TeamParticipation.teamID == admin_team_id,
- TeamParticipation.person == user)))
+ user_is_admin = Exists(
+ Select(
+ TeamParticipation.personID,
+ tables=[TeamParticipation],
+ where=And(
+ TeamParticipation.teamID == admin_team_id,
+ TeamParticipation.person == user,
+ ),
+ )
+ )
return Or(
- private_recipe == False, artifact_grant_query, policy_grant_query,
- user_is_admin)
+ private_recipe == False,
+ artifact_grant_query,
+ policy_grant_query,
+ user_is_admin,
+ )
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index c01448d..e62d8b8 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -4,10 +4,10 @@
"""A build record for OCI Recipes."""
__all__ = [
- 'OCIFile',
- 'OCIRecipeBuild',
- 'OCIRecipeBuildSet',
- ]
+ "OCIFile",
+ "OCIRecipeBuild",
+ "OCIRecipeBuildSet",
+]
from datetime import timedelta
@@ -22,7 +22,7 @@ from storm.locals import (
Reference,
Store,
Unicode,
- )
+)
from storm.store import EmptyResultSet
from zope.component import getUtility
from zope.interface import implementer
@@ -33,7 +33,7 @@ from lp.buildmaster.enums import (
BuildFarmJobType,
BuildQueueStatus,
BuildStatus,
- )
+)
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
from lp.buildmaster.model.packagebuild import PackageBuildMixin
@@ -46,12 +46,12 @@ from lp.oci.interfaces.ocirecipebuild import (
IOCIRecipeBuild,
IOCIRecipeBuildSet,
OCIRecipeBuildRegistryUploadStatus,
- )
+)
from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
from lp.oci.model.ocirecipebuildjob import (
OCIRecipeBuildJob,
OCIRecipeBuildJobType,
- )
+)
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.person import Person
@@ -60,48 +60,40 @@ from lp.services.database.bulk import load_related
from lp.services.database.constants import DEFAULT
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
from lp.services.librarian.browser import ProxiedLibraryFileAlias
-from lp.services.librarian.model import (
- LibraryFileAlias,
- LibraryFileContent,
- )
+from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
from lp.services.macaroons.interfaces import (
+ NO_USER,
BadMacaroonContext,
IMacaroonIssuer,
- NO_USER,
- )
+)
from lp.services.macaroons.model import MacaroonIssuerBase
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp.snapshot import notify_modified
@implementer(IOCIFile)
class OCIFile(StormBase):
- __storm_table__ = 'OCIFile'
+ __storm_table__ = "OCIFile"
- id = Int(name='id', primary=True)
+ id = Int(name="id", primary=True)
- build_id = Int(name='build', allow_none=False)
- build = Reference(build_id, 'OCIRecipeBuild.id')
+ build_id = Int(name="build", allow_none=False)
+ build = Reference(build_id, "OCIRecipeBuild.id")
- library_file_id = Int(name='library_file', allow_none=False)
- library_file = Reference(library_file_id, 'LibraryFileAlias.id')
+ library_file_id = Int(name="library_file", allow_none=False)
+ library_file = Reference(library_file_id, "LibraryFileAlias.id")
- layer_file_digest = Unicode(name='layer_file_digest', allow_none=True)
+ layer_file_digest = Unicode(name="layer_file_digest", allow_none=True)
date_last_used = DateTime(
- name='date_last_used', tzinfo=pytz.UTC, allow_none=False)
+ name="date_last_used", tzinfo=pytz.UTC, allow_none=False
+ )
def __init__(self, build, library_file, layer_file_digest=None):
"""Construct a `OCIFile`."""
@@ -113,67 +105,78 @@ class OCIFile(StormBase):
@implementer(IOCIFileSet)
class OCIFileSet:
-
def getByLayerDigest(self, layer_file_digest):
- return IStore(OCIFile).find(
- OCIFile,
- OCIFile.layer_file_digest == layer_file_digest).order_by(
- OCIFile.id).first()
+ return (
+ IStore(OCIFile)
+ .find(OCIFile, OCIFile.layer_file_digest == layer_file_digest)
+ .order_by(OCIFile.id)
+ .first()
+ )
@implementer(IOCIRecipeBuild)
class OCIRecipeBuild(PackageBuildMixin, StormBase):
- __storm_table__ = 'OCIRecipeBuild'
+ __storm_table__ = "OCIRecipeBuild"
job_type = BuildFarmJobType.OCIRECIPEBUILD
- id = Int(name='id', primary=True)
+ id = Int(name="id", primary=True)
- build_request_id = Int(name='build_request', allow_none=True)
+ build_request_id = Int(name="build_request", allow_none=True)
- requester_id = Int(name='requester', allow_none=False)
- requester = Reference(requester_id, 'Person.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')
+ 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')
+ processor_id = Int(name="processor", allow_none=False)
+ processor = Reference(processor_id, "Processor.id")
- virtualized = Bool(name='virtualized')
+ 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)
+ 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)
+ name="date_first_dispatched", tzinfo=pytz.UTC
+ )
- builder_id = Int(name='builder')
- builder = Reference(builder_id, 'Builder.id')
+ builder_id = Int(name="builder")
+ builder = Reference(builder_id, "Builder.id")
- status = DBEnum(name='status', enum=BuildStatus, allow_none=False)
+ status = DBEnum(name="status", enum=BuildStatus, allow_none=False)
- log_id = Int(name='log')
- log = Reference(log_id, 'LibraryFileAlias.id')
+ 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')
+ upload_log_id = Int(name="upload_log")
+ upload_log = Reference(upload_log_id, "LibraryFileAlias.id")
- dependencies = Unicode(name='dependencies')
+ dependencies = Unicode(name="dependencies")
- failure_count = Int(name='failure_count', allow_none=False)
+ failure_count = Int(name="failure_count", allow_none=False)
- build_farm_job_id = Int(name='build_farm_job', allow_none=False)
- build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id')
+ build_farm_job_id = Int(name="build_farm_job", allow_none=False)
+ build_farm_job = Reference(build_farm_job_id, "BuildFarmJob.id")
# We only care about the pocket from a building environment POV,
# it is not a target, nor referenced in the final build.
pocket = PackagePublishingPocket.UPDATES
- def __init__(self, build_farm_job, requester, recipe,
- processor, virtualized, date_created, build_request=None):
+ def __init__(
+ self,
+ build_farm_job,
+ requester,
+ recipe,
+ processor,
+ virtualized,
+ date_created,
+ build_request=None,
+ ):
"""Construct an `OCIRecipeBuild`."""
self.requester = requester
self.recipe = recipe
@@ -193,17 +196,24 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
def __repr__(self):
return "<OCIRecipeBuild ~%s/%s/+oci/%s/+recipe/%s/+build/%d>" % (
- self.recipe.owner.name, self.recipe.oci_project.pillar.name,
- self.recipe.oci_project.name, self.recipe.name, self.id)
+ self.recipe.owner.name,
+ self.recipe.oci_project.pillar.name,
+ self.recipe.oci_project.name,
+ self.recipe.name,
+ self.id,
+ )
@property
def title(self):
# XXX cjwatson 2020-02-19: This should use a DAS architecture tag
# rather than a processor name once we can do that.
return "%s build of /~%s/%s/+oci/%s/+recipe/%s" % (
- self.processor.name, self.recipe.owner.name,
- self.recipe.oci_project.pillar.name, self.recipe.oci_project.name,
- self.recipe.name)
+ self.processor.name,
+ self.recipe.owner.name,
+ self.recipe.oci_project.pillar.name,
+ self.recipe.oci_project.name,
+ self.recipe.name,
+ )
@property
def score(self):
@@ -234,10 +244,11 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
# https://code.launchpad.net/
# ~cjwatson/launchpad/snap-build-record-code/+merge/365356
return (
- self.recipe.private or
- self.recipe.owner.private or
- self.recipe.git_repository is None or
- self.recipe.git_repository.private)
+ self.recipe.private
+ or self.recipe.owner.private
+ or self.recipe.git_repository is None
+ or self.recipe.git_repository.private
+ )
private = is_private
@@ -259,7 +270,8 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
(OCIRecipeBuild.date_started, OCIRecipeBuild.date_finished),
OCIRecipeBuild.recipe == self.recipe_id,
OCIRecipeBuild.processor == self.processor_id,
- OCIRecipeBuild.status == BuildStatus.FULLYBUILT)
+ OCIRecipeBuild.status == BuildStatus.FULLYBUILT,
+ )
result.order_by(Desc(OCIRecipeBuild.date_finished))
durations = [row[1] - row[0] for row in result[:9]]
if len(durations) == 0:
@@ -273,7 +285,8 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
(OCIFile, LibraryFileAlias, LibraryFileContent),
OCIFile.build == self.id,
LibraryFileAlias.id == OCIFile.library_file_id,
- LibraryFileContent.id == LibraryFileAlias.contentID)
+ LibraryFileContent.id == LibraryFileAlias.contentID,
+ )
return result.order_by([LibraryFileAlias.filename, OCIFile.id])
def getFileByName(self, filename):
@@ -281,13 +294,22 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
origin = [
LibraryFileAlias,
LeftJoin(OCIFile, LibraryFileAlias.id == OCIFile.library_file_id),
- ]
- file_object = Store.of(self).using(*origin).find(
- LibraryFileAlias,
- Or(
- LibraryFileAlias.id.is_in((self.log_id, self.upload_log_id)),
- OCIFile.build == self.id),
- LibraryFileAlias.filename == filename).one()
+ ]
+ file_object = (
+ Store.of(self)
+ .using(*origin)
+ .find(
+ LibraryFileAlias,
+ Or(
+ LibraryFileAlias.id.is_in(
+ (self.log_id, self.upload_log_id)
+ ),
+ OCIFile.build == self.id,
+ ),
+ LibraryFileAlias.filename == filename,
+ )
+ .one()
+ )
if file_object is not None and file_object.filename == filename:
return file_object
@@ -353,23 +375,34 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
@property
def distro_arch_series(self):
return self.recipe.distro_series.getDistroArchSeriesByProcessor(
- self.processor)
+ self.processor
+ )
@property
def arch_tag(self):
"""See `IOCIRecipeBuild`."""
return self.distro_arch_series.architecturetag
- def updateStatus(self, status, builder=None, worker_status=None,
- date_started=None, date_finished=None,
- force_invalid_transition=False):
+ def updateStatus(
+ self,
+ status,
+ builder=None,
+ worker_status=None,
+ date_started=None,
+ date_finished=None,
+ force_invalid_transition=False,
+ ):
"""See `IBuildFarmJob`."""
edited_fields = set()
with notify_modified(self, edited_fields) as previous_obj:
super().updateStatus(
- status, builder=builder, worker_status=worker_status,
- date_started=date_started, date_finished=date_finished,
- force_invalid_transition=force_invalid_transition)
+ status,
+ builder=builder,
+ worker_status=worker_status,
+ date_started=date_started,
+ date_finished=date_finished,
+ force_invalid_transition=force_invalid_transition,
+ )
if self.status != previous_obj.status:
edited_fields.add("status")
# notify_modified evaluates all attributes mentioned in the
@@ -385,19 +418,25 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
# XXX twom 2019-12-11 This should send mail
def getLayerFileByDigest(self, layer_file_digest):
- file_object = Store.of(self).find(
- (OCIFile, LibraryFileAlias, LibraryFileContent),
- OCIFile.build == self.id,
- LibraryFileAlias.id == OCIFile.library_file_id,
- LibraryFileContent.id == LibraryFileAlias.contentID,
- OCIFile.layer_file_digest == layer_file_digest).one()
+ file_object = (
+ Store.of(self)
+ .find(
+ (OCIFile, LibraryFileAlias, LibraryFileContent),
+ OCIFile.build == self.id,
+ LibraryFileAlias.id == OCIFile.library_file_id,
+ LibraryFileContent.id == LibraryFileAlias.contentID,
+ OCIFile.layer_file_digest == layer_file_digest,
+ )
+ .one()
+ )
if file_object is not None:
return file_object
raise NotFoundError(layer_file_digest)
def addFile(self, lfa, layer_file_digest=None):
oci_file = OCIFile(
- build=self, library_file=lfa, layer_file_digest=layer_file_digest)
+ build=self, library_file=lfa, layer_file_digest=layer_file_digest
+ )
IMasterStore(OCIFile).add(oci_file)
return oci_file
@@ -420,10 +459,12 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
layer_files = Store.of(self).find(
OCIFile,
OCIFile.build == self.id,
- OCIFile.layer_file_digest is not None)
+ OCIFile.layer_file_digest is not None,
+ )
layer_files_present = not layer_files.is_empty()
- metadata_present = (self.manifest is not None
- and self.digests is not None)
+ metadata_present = (
+ self.manifest is not None and self.digests is not None
+ )
return layer_files_present and metadata_present
@property
@@ -431,7 +472,8 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
jobs = Store.of(self).find(
OCIRecipeBuildJob,
OCIRecipeBuildJob.build == self,
- OCIRecipeBuildJob.job_type == OCIRecipeBuildJobType.REGISTRY_UPLOAD
+ OCIRecipeBuildJob.job_type
+ == OCIRecipeBuildJobType.REGISTRY_UPLOAD,
)
jobs.order_by(Desc(OCIRecipeBuildJob.job_id))
@@ -439,7 +481,8 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
load_related(Job, rows, ["job_id"])
return DecoratedResultSet(
- jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs)
+ jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs
+ )
@cachedproperty
def last_registry_upload_job(self):
@@ -474,21 +517,29 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
if not self.recipe.can_upload_to_registry:
raise CannotScheduleRegistryUpload(
"Cannot upload this build to registries because the recipe is "
- "not properly configured.")
+ "not properly configured."
+ )
if not self.was_built or self.getFiles().is_empty():
raise CannotScheduleRegistryUpload(
- "Cannot upload this build because it has no files.")
- if (self.registry_upload_status ==
- OCIRecipeBuildRegistryUploadStatus.PENDING):
+ "Cannot upload this build because it has no files."
+ )
+ if (
+ self.registry_upload_status
+ == OCIRecipeBuildRegistryUploadStatus.PENDING
+ ):
raise CannotScheduleRegistryUpload(
- "An upload of this build is already in progress.")
- elif (self.registry_upload_status ==
- OCIRecipeBuildRegistryUploadStatus.UPLOADED):
+ "An upload of this build is already in progress."
+ )
+ elif (
+ self.registry_upload_status
+ == OCIRecipeBuildRegistryUploadStatus.UPLOADED
+ ):
# XXX cjwatson 2020-04-22: This won't be quite right in the case
# where a recipe has multiple push rules.
raise CannotScheduleRegistryUpload(
"Cannot upload this build because it has already been "
- "uploaded.")
+ "uploaded."
+ )
getUtility(IOCIRegistryUploadJobSource).create(self)
def hasMoreRecentBuild(self):
@@ -498,7 +549,8 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
OCIRecipeBuild.recipe == self.recipe,
OCIRecipeBuild.processor == self.processor,
OCIRecipeBuild.status == BuildStatus.FULLYBUILT,
- OCIRecipeBuild.date_created > self.date_created)
+ OCIRecipeBuild.date_created > self.date_created,
+ )
return not recent_builds.is_empty()
@@ -506,19 +558,33 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
"""See `IOCIRecipeBuildSet`."""
- def new(self, requester, recipe, distro_arch_series,
- date_created=DEFAULT, build_request=None):
+ def new(
+ self,
+ requester,
+ recipe,
+ distro_arch_series,
+ date_created=DEFAULT,
+ build_request=None,
+ ):
"""See `IOCIRecipeBuildSet`."""
virtualized = (
not distro_arch_series.processor.supports_nonvirtualized
- or recipe.require_virtualized)
+ or recipe.require_virtualized
+ )
store = IMasterStore(OCIRecipeBuild)
build_farm_job = getUtility(IBuildFarmJobSource).new(
- OCIRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created)
+ OCIRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created
+ )
ocirecipebuild = OCIRecipeBuild(
- build_farm_job, requester, recipe, distro_arch_series.processor,
- virtualized, date_created, build_request=build_request)
+ build_farm_job,
+ requester,
+ recipe,
+ distro_arch_series.processor,
+ virtualized,
+ date_created,
+ build_request=build_request,
+ )
store.add(ocirecipebuild)
store.flush()
return ocirecipebuild
@@ -527,6 +593,7 @@ class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
"""See `IOCIRecipeBuildSet`."""
# Circular import.
from lp.oci.model.ocirecipe import OCIRecipe
+
load_related(Person, builds, ["requester_id"])
lfas = load_related(LibraryFileAlias, builds, ["log_id"])
load_related(LibraryFileContent, lfas, ["contentID"])
@@ -543,16 +610,22 @@ class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
def getByBuildFarmJob(self, build_farm_job):
"""See `ISpecificBuildFarmJobSource`."""
- return Store.of(build_farm_job).find(
- OCIRecipeBuild, build_farm_job_id=build_farm_job.id).one()
+ return (
+ Store.of(build_farm_job)
+ .find(OCIRecipeBuild, build_farm_job_id=build_farm_job.id)
+ .one()
+ )
def getByBuildFarmJobs(self, build_farm_jobs):
"""See `ISpecificBuildFarmJobSource`."""
if len(build_farm_jobs) == 0:
return EmptyResultSet()
rows = Store.of(build_farm_jobs[0]).find(
- OCIRecipeBuild, OCIRecipeBuild.build_farm_job_id.is_in(
- bfj.id for bfj in build_farm_jobs))
+ OCIRecipeBuild,
+ OCIRecipeBuild.build_farm_job_id.is_in(
+ bfj.id for bfj in build_farm_jobs
+ ),
+ )
return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
@@ -571,7 +644,8 @@ class OCIRecipeBuildMacaroonIssuer(MacaroonIssuerBase):
raise BadMacaroonContext(context)
if not removeSecurityProxy(context).is_private:
raise BadMacaroonContext(
- context, "Refusing to issue macaroon for public build.")
+ context, "Refusing to issue macaroon for public build."
+ )
return removeSecurityProxy(context).id
def checkVerificationContext(self, context, **kwargs):
@@ -580,8 +654,9 @@ class OCIRecipeBuildMacaroonIssuer(MacaroonIssuerBase):
raise BadMacaroonContext(context)
return context
- def verifyPrimaryCaveat(self, verified, caveat_value, context, user=None,
- **kwargs):
+ def verifyPrimaryCaveat(
+ self, verified, caveat_value, context, user=None, **kwargs
+ ):
"""See `MacaroonIssuerBase`.
For verification, the context is an `IGitRepository`. We check that
@@ -607,9 +682,14 @@ class OCIRecipeBuildMacaroonIssuer(MacaroonIssuerBase):
build_id = int(caveat_value)
except ValueError:
return False
- return not IStore(OCIRecipeBuild).find(
- OCIRecipeBuild,
- OCIRecipeBuild.id == build_id,
- OCIRecipeBuild.recipe_id == OCIRecipe.id,
- OCIRecipe.git_repository == context,
- OCIRecipeBuild.status == BuildStatus.BUILDING).is_empty()
+ return (
+ not IStore(OCIRecipeBuild)
+ .find(
+ OCIRecipeBuild,
+ OCIRecipeBuild.id == build_id,
+ OCIRecipeBuild.recipe_id == OCIRecipe.id,
+ OCIRecipe.git_repository == context,
+ OCIRecipeBuild.status == BuildStatus.BUILDING,
+ )
+ .is_empty()
+ )
diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
index 6939ca5..d35259c 100644
--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
@@ -7,13 +7,13 @@ Dispatches OCI image build jobs to build-farm workers.
"""
__all__ = [
- 'OCIRecipeBuildBehaviour',
- ]
+ "OCIRecipeBuildBehaviour",
+]
-from datetime import datetime
import json
import os
+from datetime import datetime
import pytz
from twisted.internet import defer
@@ -23,16 +23,13 @@ from zope.security.proxy import removeSecurityProxy
from lp.buildmaster.builderproxy import BuilderProxyMixin
from lp.buildmaster.enums import BuildBaseImageType
-from lp.buildmaster.interfaces.builder import (
- BuildDaemonError,
- CannotBuild,
- )
+from lp.buildmaster.interfaces.builder import BuildDaemonError, CannotBuild
from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
IBuildFarmJobBehaviour,
- )
+)
from lp.buildmaster.model.buildfarmjobbehaviour import (
BuildFarmJobBehaviourBase,
- )
+)
from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
from lp.oci.interfaces.ocirecipebuild import IOCIFileSet
from lp.registry.interfaces.series import SeriesStatus
@@ -40,9 +37,7 @@ from lp.services.config import config
from lp.services.librarian.utils import copy_and_close
from lp.services.twistedsupport import cancel_on_timeout
from lp.services.webapp import canonical_url
-from lp.soyuz.adapters.archivedependencies import (
- get_sources_list_for_building,
- )
+from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
@implementer(IBuildFarmJobBehaviour)
@@ -56,10 +51,13 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# Examples:
# buildlog_oci_ubuntu_wily_amd64_name_FULLYBUILT.txt
- return 'buildlog_oci_%s_%s_%s_%s_%s.txt' % (
- series.distribution.name, series.name,
- self.build.processor.name, self.build.recipe.name,
- self.build.status.name)
+ return "buildlog_oci_%s_%s_%s_%s_%s.txt" % (
+ series.distribution.name,
+ series.name,
+ self.build.processor.name,
+ self.build.recipe.name,
+ self.build.status.name,
+ )
def verifyBuildRequest(self, logger):
"""Assert some pre-build checks.
@@ -71,20 +69,26 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
build = self.build
if build.virtualized and not self._builder.virtualized:
raise AssertionError(
- "Attempt to build virtual item on a non-virtual builder.")
+ "Attempt to build virtual item on a non-virtual builder."
+ )
chroot = build.distro_arch_series.getChroot(pocket=build.pocket)
if chroot is None:
raise CannotBuild(
- "Missing chroot for %s" % build.distro_arch_series.displayname)
+ "Missing chroot for %s" % build.distro_arch_series.displayname
+ )
def issueMacaroon(self):
"""See `IBuildFarmJobBehaviour`."""
return cancel_on_timeout(
self._authserver.callRemote(
"issueMacaroon",
- "oci-recipe-build", "OCIRecipeBuild", self.build.id),
- config.builddmaster.authentication_timeout)
+ "oci-recipe-build",
+ "OCIRecipeBuild",
+ self.build.id,
+ ),
+ config.builddmaster.authentication_timeout,
+ )
def _getBuildInfoArgs(self):
def format_user(user):
@@ -93,7 +97,9 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
hide_email = not user.preferredemail or user.hide_email_addresses
return {
"name": user.name,
- "email": (None if hide_email else user.preferredemail.email)}
+ "email": (None if hide_email else user.preferredemail.email),
+ }
+
build = self.build
build_request = build.build_request
builds = list(build_request.builds) if build_request else [build]
@@ -111,13 +117,16 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
}
if build_request:
info["build_request_id"] = build_request.id
- info["build_request_timestamp"] = (
- build_request.date_requested.isoformat())
- info["architectures"] = [i.distro_arch_series.architecturetag
- for i in builds]
+ info[
+ "build_request_timestamp"
+ ] = build_request.date_requested.isoformat()
+ info["architectures"] = [
+ i.distro_arch_series.architecturetag for i in builds
+ ]
info["build_urls"] = {
i.distro_arch_series.architecturetag: canonical_url(i)
- for i in builds}
+ for i in builds
+ }
return info
@defer.inlineCallbacks
@@ -131,39 +140,49 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# XXX twom 2020-02-17 This may need to be more complex, and involve
# distribution name.
args["name"] = build.recipe.name
- args["archives"], args["trusted_keys"] = (
- yield get_sources_list_for_building(
- self, build.distro_arch_series, None,
- tools_source=None, tools_fingerprint=None,
- logger=logger))
-
- args['build_file'] = build.recipe.build_file
+ (
+ args["archives"],
+ args["trusted_keys"],
+ ) = yield get_sources_list_for_building(
+ self,
+ build.distro_arch_series,
+ None,
+ tools_source=None,
+ tools_fingerprint=None,
+ logger=logger,
+ )
+
+ args["build_file"] = build.recipe.build_file
# Do our work on a new dict, so we don't try to update the
# copy on the model
build_args = {
- "LAUNCHPAD_BUILD_ARCH": build.distro_arch_series.architecturetag}
+ "LAUNCHPAD_BUILD_ARCH": build.distro_arch_series.architecturetag
+ }
# We have to remove the security proxy that Zope applies to this
# dict, since otherwise we'll be unable to serialise it to
# XML-RPC.
build_args.update(removeSecurityProxy(build.recipe.build_args))
- args['build_args'] = build_args
- args['build_path'] = build.recipe.build_path
- args['metadata'] = self._getBuildInfoArgs()
+ args["build_args"] = build_args
+ args["build_path"] = build.recipe.build_path
+ args["metadata"] = self._getBuildInfoArgs()
if build.recipe.git_ref is not None:
if build.recipe.git_repository.private:
macaroon_raw = yield self.issueMacaroon()
url = build.recipe.git_repository.getCodebrowseUrl(
- username=LAUNCHPAD_SERVICES, password=macaroon_raw)
+ username=LAUNCHPAD_SERVICES, password=macaroon_raw
+ )
args["git_repository"] = url
else:
- args["git_repository"] = (
- build.recipe.git_repository.git_https_url)
+ args[
+ "git_repository"
+ ] = build.recipe.git_repository.git_https_url
else:
raise CannotBuild(
- "Source repository for ~%s/%s has been deleted." %
- (build.recipe.owner.name, build.recipe.name))
+ "Source repository for ~%s/%s has been deleted."
+ % (build.recipe.owner.name, build.recipe.name)
+ )
if build.recipe.git_path != "HEAD":
args["git_path"] = build.recipe.git_ref.name
@@ -174,9 +193,10 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# If the evaluated output file name is not within our
# upload path, then we don't try to copy this or any
# subsequent files.
- if not os.path.normpath(file_path).startswith(upload_path + '/'):
+ if not os.path.normpath(file_path).startswith(upload_path + "/"):
raise BuildDaemonError(
- "Build returned a file named '%s'." % file_name)
+ "Build returned a file named '%s'." % file_name
+ )
@defer.inlineCallbacks
def _fetchIntermediaryFile(self, name, filemap, upload_path):
@@ -191,15 +211,14 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
def _extractLayerFiles(self, upload_path, section, config, digests, files):
# These are different sets of ids, in the same order
# layer_id is the filename, diff_id is the internal (docker) id
- for diff_id in config['rootfs']['diff_ids']:
+ for diff_id in config["rootfs"]["diff_ids"]:
for digests_section in digests:
- layer_id = digests_section[diff_id]['layer_id']
+ layer_id = digests_section[diff_id]["layer_id"]
# This is in the form '<id>/layer.tar', we only need the first
- layer_filename = "{}.tar.gz".format(layer_id.split('/')[0])
- digest = digests_section[diff_id]['digest']
+ layer_filename = "{}.tar.gz".format(layer_id.split("/")[0])
+ digest = digests_section[diff_id]["digest"]
# Check if the file already exists in the librarian
- oci_file = getUtility(IOCIFileSet).getByLayerDigest(
- digest)
+ oci_file = getUtility(IOCIFileSet).getByLayerDigest(digest)
if oci_file:
librarian_file = oci_file.library_file
unsecure_file = removeSecurityProxy(oci_file)
@@ -212,7 +231,7 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# so we can add it to the build artifacts
layer_path = os.path.join(upload_path, layer_filename)
librarian_file.open()
- copy_and_close(librarian_file, open(layer_path, 'wb'))
+ copy_and_close(librarian_file, open(layer_path, "wb"))
def _convertToRetrievableFile(self, upload_path, file_name, filemap):
file_path = os.path.join(upload_path, file_name)
@@ -227,20 +246,25 @@ class OCIRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# We don't want to download all of the files that have been created,
# just the ones that are mentioned in the manifest and config.
manifest = yield self._fetchIntermediaryFile(
- 'manifest.json', filemap, upload_path)
+ "manifest.json", filemap, upload_path
+ )
digests = yield self._fetchIntermediaryFile(
- 'digests.json', filemap, upload_path)
+ "digests.json", filemap, upload_path
+ )
files = set()
for section in manifest:
config = yield self._fetchIntermediaryFile(
- section['Config'], filemap, upload_path)
+ section["Config"], filemap, upload_path
+ )
self._extractLayerFiles(
- upload_path, section, config, digests, files)
+ upload_path, section, config, digests, files
+ )
files_to_download = [
self._convertToRetrievableFile(upload_path, filename, filemap)
- for filename in files]
+ for filename in files
+ ]
yield self._worker.getFiles(files_to_download, logger=logger)
def verifySuccessfulBuild(self):
diff --git a/lib/lp/oci/model/ocirecipebuildjob.py b/lib/lp/oci/model/ocirecipebuildjob.py
index 4d7f098..f8d4f3c 100644
--- a/lib/lp/oci/model/ocirecipebuildjob.py
+++ b/lib/lp/oci/model/ocirecipebuildjob.py
@@ -4,41 +4,31 @@
"""OCIRecipe build jobs."""
__all__ = [
- 'OCIRecipeBuildJob',
- 'OCIRecipeBuildJobType',
- ]
+ "OCIRecipeBuildJob",
+ "OCIRecipeBuildJobType",
+]
-from datetime import timedelta
import random
+from datetime import timedelta
+import transaction
from lazr.delegates import delegate_to
-from lazr.enum import (
- DBEnumeratedType,
- DBItem,
- )
+from lazr.enum import DBEnumeratedType, DBItem
from storm.databases.postgres import JSON
-from storm.locals import (
- Int,
- Reference,
- Store,
- )
-import transaction
+from storm.locals import Int, Reference, Store
from zope.component import getUtility
-from zope.interface import (
- implementer,
- provider,
- )
+from zope.interface import implementer, provider
from lp.app.errors import NotFoundError
from lp.oci.interfaces.ocirecipebuildjob import (
IOCIRecipeBuildJob,
IOCIRegistryUploadJob,
IOCIRegistryUploadJobSource,
- )
+)
from lp.oci.interfaces.ociregistryclient import (
IOCIRegistryClient,
OCIRegistryError,
- )
+)
from lp.services.config import config
from lp.services.database.enumcol import DBEnum
from lp.services.database.interfaces import IStore
@@ -46,13 +36,10 @@ from lp.services.database.locking import (
AdvisoryLockHeld,
LockType,
try_advisory_lock,
- )
+)
from lp.services.database.stormbase import StormBase
from lp.services.job.interfaces.job import JobStatus
-from lp.services.job.model.job import (
- EnumeratedSubclass,
- Job,
- )
+from lp.services.job.model.job import EnumeratedSubclass, Job
from lp.services.job.runner import BaseRunnableJob
from lp.services.propertycache import get_property_cache
from lp.services.webapp.snapshot import notify_modified
@@ -61,28 +48,31 @@ from lp.services.webapp.snapshot import notify_modified
class OCIRecipeBuildJobType(DBEnumeratedType):
"""Values that `OCIBuildJobType.job_type` can take."""
- REGISTRY_UPLOAD = DBItem(0, """
+ REGISTRY_UPLOAD = DBItem(
+ 0,
+ """
Registry upload
This job uploads an OCI Image to the registry.
- """)
+ """,
+ )
@implementer(IOCIRecipeBuildJob)
class OCIRecipeBuildJob(StormBase):
"""See `IOCIRecipeBuildJob`."""
- __storm_table__ = 'OCIRecipeBuildJob'
+ __storm_table__ = "OCIRecipeBuildJob"
- job_id = Int(name='job', primary=True, allow_none=False)
- job = Reference(job_id, 'Job.id')
+ job_id = Int(name="job", primary=True, allow_none=False)
+ job = Reference(job_id, "Job.id")
- build_id = Int(name='build', allow_none=False)
- build = Reference(build_id, 'OCIRecipeBuild.id')
+ build_id = Int(name="build", allow_none=False)
+ build = Reference(build_id, "OCIRecipeBuild.id")
job_type = DBEnum(enum=OCIRecipeBuildJobType, allow_none=True)
- json_data = JSON('json_data', allow_none=False)
+ json_data = JSON("json_data", allow_none=False)
def __init__(self, build, job_type, json_data, **job_args):
"""Constructor.
@@ -107,7 +97,6 @@ class OCIRecipeBuildJob(StormBase):
@delegate_to(IOCIRecipeBuildJob)
class OCIRecipeBuildJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
-
def __init__(self, oci_build_job):
self.context = oci_build_job
@@ -116,9 +105,13 @@ class OCIRecipeBuildJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
try:
build = self.build
return "<%s for ~%s/%s/+oci/%s/+recipe/%s/+build/%d>" % (
- self.__class__.__name__, build.recipe.owner.name,
+ self.__class__.__name__,
+ build.recipe.owner.name,
build.recipe.oci_project.pillar.name,
- build.recipe.oci_project.name, build.recipe.name, build.id)
+ build.recipe.oci_project.name,
+ build.recipe.name,
+ build.id,
+ )
except Exception:
# There might be errors while trying to do the full
# representation of this object (database transaction errors,
@@ -137,11 +130,13 @@ class OCIRecipeBuildJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
or its `job_type` does not match the desired subclass.
"""
oci_build_job = IStore(OCIRecipeBuildJob).get(
- OCIRecipeBuildJob, job_id)
+ OCIRecipeBuildJob, job_id
+ )
if oci_build_job.job_type != cls.class_job_type:
raise NotFoundError(
- "No object found with id %d and type %s" %
- (job_id, cls.class_job_type.title))
+ "No object found with id %d and type %s"
+ % (job_id, cls.class_job_type.title)
+ )
return cls(oci_build_job)
@classmethod
@@ -151,18 +146,24 @@ class OCIRecipeBuildJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
OCIRecipeBuildJob,
OCIRecipeBuildJob.job_type == cls.class_job_type,
OCIRecipeBuildJob.job == Job.id,
- Job.id.is_in(Job.ready_jobs))
+ Job.id.is_in(Job.ready_jobs),
+ )
return (cls(job) for job in jobs)
def getOopsVars(self):
"""See `IRunnableJob`."""
oops_vars = super().getOopsVars()
- oops_vars.extend([
- ('job_type', self.context.job_type.title),
- ('build_id', self.context.build.id),
- ('recipe_owner_id', self.context.build.recipe.owner.id),
- ('oci_project_name', self.context.build.recipe.oci_project.name)
- ])
+ oops_vars.extend(
+ [
+ ("job_type", self.context.job_type.title),
+ ("build_id", self.context.build.id),
+ ("recipe_owner_id", self.context.build.recipe.owner.id),
+ (
+ "oci_project_name",
+ self.context.build.recipe.oci_project.name,
+ ),
+ ]
+ )
return oops_vars
@@ -183,7 +184,7 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
# This is a known slow task that will exceed the timeouts for
# the normal job queue, so put it on a queue with longer timeouts
- task_queue = 'launchpad_job_slow'
+ task_queue = "launchpad_job_slow"
soft_time_limit = timedelta(minutes=60)
lease_duration = timedelta(minutes=60)
@@ -191,7 +192,10 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
class ManifestListUploadError(Exception):
pass
- retry_error_types = (ManifestListUploadError, AdvisoryLockHeld,)
+ retry_error_types = (
+ ManifestListUploadError,
+ AdvisoryLockHeld,
+ )
max_retries = 5
config = config.IOCIRegistryUploadJobSource
@@ -205,7 +209,8 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
"build_uploaded": False,
}
oci_build_job = OCIRecipeBuildJob(
- build, cls.class_job_type, json_data)
+ build, cls.class_job_type, json_data
+ )
job = cls(oci_build_job)
job.celeryRunOnCommit()
del get_property_cache(build).last_registry_upload_job
@@ -220,23 +225,25 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
delays = (10, 15, 20, 30)
try:
return timedelta(
- minutes=delays[self.attempt_count - 1],
- seconds=dithering_secs)
+ minutes=delays[self.attempt_count - 1], seconds=dithering_secs
+ )
except IndexError:
return timedelta(minutes=10, seconds=dithering_secs)
# Ideally we'd just override Job._set_status or similar, but
# lazr.delegates makes that difficult, so we use this to override all
# the individual Job lifecycle methods instead.
- def _do_lifecycle(self, method_name, manage_transaction=False,
- *args, **kwargs):
+ def _do_lifecycle(
+ self, method_name, manage_transaction=False, *args, **kwargs
+ ):
edited_fields = set()
with notify_modified(self.build, edited_fields) as before_modification:
getattr(super(), method_name)(
- *args, manage_transaction=manage_transaction, **kwargs)
+ *args, manage_transaction=manage_transaction, **kwargs
+ )
upload_status = self.build.registry_upload_status
if upload_status != before_modification.registry_upload_status:
- edited_fields.add('registry_upload_status')
+ edited_fields.add("registry_upload_status")
if edited_fields and manage_transaction:
transaction.commit()
@@ -302,9 +309,9 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
builds = set()
for build, upload_jobs in uploads_per_build.items():
has_finished_upload = any(
- i.status == JobStatus.COMPLETED
- or i.job_id == self.job_id
- for i in upload_jobs)
+ i.status == JobStatus.COMPLETED or i.job_id == self.job_id
+ for i in upload_jobs
+ )
if has_finished_upload:
builds.add(build)
return builds
@@ -330,9 +337,11 @@ class OCIRegistryUploadJob(OCIRecipeBuildJobDerived):
"""See `IRunnableJob`."""
client = getUtility(IOCIRegistryClient)
try:
- with try_advisory_lock(LockType.REGISTRY_UPLOAD,
- self.build.recipe.id,
- Store.of(self.build.recipe)):
+ with try_advisory_lock(
+ LockType.REGISTRY_UPLOAD,
+ self.build.recipe.id,
+ Store.of(self.build.recipe),
+ ):
try:
if not self.build_uploaded:
client.upload(self.build)
diff --git a/lib/lp/oci/model/ocirecipejob.py b/lib/lp/oci/model/ocirecipejob.py
index 4ba7d5e..f628f43 100644
--- a/lib/lp/oci/model/ocirecipejob.py
+++ b/lib/lp/oci/model/ocirecipejob.py
@@ -4,25 +4,19 @@
"""A build job for OCI Recipe."""
__all__ = [
- 'OCIRecipeJob',
- ]
+ "OCIRecipeJob",
+]
+import transaction
from lazr.delegates import delegate_to
-from lazr.enum import (
- DBEnumeratedType,
- DBItem,
- )
+from lazr.enum import DBEnumeratedType, DBItem
from storm.databases.postgres import JSON
from storm.expr import Desc
from storm.properties import Int
from storm.references import Reference
from storm.store import EmptyResultSet
-import transaction
from zope.component import getUtility
-from zope.interface import (
- implementer,
- provider,
- )
+from zope.interface import implementer, provider
from zope.security.proxy import removeSecurityProxy
from lp.app.errors import NotFoundError
@@ -31,27 +25,21 @@ from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.oci.interfaces.ocirecipebuild import (
OCIRecipeBuildRegistryUploadStatus,
OCIRecipeBuildSetRegistryUploadStatus,
- )
+)
from lp.oci.interfaces.ocirecipejob import (
IOCIRecipeJob,
IOCIRecipeRequestBuildsJob,
IOCIRecipeRequestBuildsJobSource,
- )
+)
from lp.oci.model.ocirecipebuild import OCIRecipeBuild
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
from lp.services.database.bulk import load_related
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import (
- EnumeratedSubclass,
- Job,
- )
+from lp.services.job.model.job import EnumeratedSubclass, Job
from lp.services.job.runner import BaseRunnableJob
from lp.services.mail.sendmail import format_address_for_person
from lp.services.propertycache import cachedproperty
@@ -62,28 +50,31 @@ from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
class OCIRecipeJobType(DBEnumeratedType):
"""Values that `IOCIRecipeJob.job_type` can take."""
- REQUEST_BUILDS = DBItem(0, """
+ REQUEST_BUILDS = DBItem(
+ 0,
+ """
Request builds
This job requests builds of an OCI recipe.
- """)
+ """,
+ )
@implementer(IOCIRecipeJob)
class OCIRecipeJob(StormBase):
"""See `IOCIRecipeJob`."""
- __storm_table__ = 'OCIRecipeJob'
+ __storm_table__ = "OCIRecipeJob"
- job_id = Int(name='job', primary=True, allow_none=False)
- job = Reference(job_id, 'Job.id')
+ job_id = Int(name="job", primary=True, allow_none=False)
+ job = Reference(job_id, "Job.id")
- recipe_id = Int(name='recipe', allow_none=False)
- recipe = Reference(recipe_id, 'OCIRecipe.id')
+ recipe_id = Int(name="recipe", allow_none=False)
+ recipe = Reference(recipe_id, "OCIRecipe.id")
job_type = DBEnum(enum=OCIRecipeJobType, allow_none=False)
- metadata = JSON('json_data', allow_none=False)
+ metadata = JSON("json_data", allow_none=False)
def __init__(self, recipe, job_type, metadata, **job_args):
"""Constructor.
@@ -108,14 +99,12 @@ class OCIRecipeJob(StormBase):
@delegate_to(IOCIRecipeJob)
class OCIRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
-
def __init__(self, recipe_job):
self.context = recipe_job
def __repr__(self):
"""An informative representation of the job."""
- return "<%s for %s>" % (
- self.__class__.__name__, self.recipe)
+ return "<%s for %s>" % (self.__class__.__name__, self.recipe)
@classmethod
def get(cls, job_id):
@@ -129,8 +118,9 @@ class OCIRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
recipe_job = IStore(IOCIRecipeJob).get(IOCIRecipeJob, job_id)
if recipe_job.job_type != cls.class_job_type:
raise NotFoundError(
- "No object found with id %d and type %s" %
- (job_id, cls.class_job_type.title))
+ "No object found with id %d and type %s"
+ % (job_id, cls.class_job_type.title)
+ )
return cls(recipe_job)
@classmethod
@@ -140,19 +130,22 @@ class OCIRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
OCIRecipeJob,
OCIRecipeJob.job_type == cls.class_job_type,
OCIRecipeJob.job == Job.id,
- Job.id.is_in(Job.ready_jobs))
+ Job.id.is_in(Job.ready_jobs),
+ )
return (cls(job) for job in jobs)
def getOopsVars(self):
"""See `IRunnableJob`."""
oops_vars = super().getOopsVars()
- oops_vars.extend([
- ("job_id", self.context.job.id),
- ("job_type", self.context.job_type.title),
- ("oci_project_name", self.context.recipe.oci_project.name),
- ("recipe_owner_name", self.context.recipe.owner.name),
- ("recipe_name", self.context.recipe.name),
- ])
+ oops_vars.extend(
+ [
+ ("job_id", self.context.job.id),
+ ("job_type", self.context.job_type.title),
+ ("oci_project_name", self.context.recipe.oci_project.name),
+ ("recipe_owner_name", self.context.recipe.owner.name),
+ ("recipe_name", self.context.recipe.name),
+ ]
+ )
return oops_vars
@@ -173,9 +166,10 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
metadata = {
"requester": requester.id,
"architectures": (
- list(architectures) if architectures is not None else None),
+ list(architectures) if architectures is not None else None
+ ),
# A dict of build_id: manifest location
- "uploaded_manifests": {}
+ "uploaded_manifests": {},
}
recipe_job = OCIRecipeJob(recipe, cls.class_job_type, metadata)
job = cls(recipe_job)
@@ -184,10 +178,15 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
@classmethod
def getByOCIRecipeAndID(cls, recipe, job_id):
- job = IStore(OCIRecipeJob).find(
- OCIRecipeJob,
- OCIRecipeJob.recipe == recipe,
- OCIRecipeJob.job_id == job_id).one()
+ job = (
+ IStore(OCIRecipeJob)
+ .find(
+ OCIRecipeJob,
+ OCIRecipeJob.recipe == recipe,
+ OCIRecipeJob.job_id == job_id,
+ )
+ .one()
+ )
if job is None:
raise NotFoundError("Could not find job ID %s" % job_id)
return cls(job)
@@ -196,21 +195,24 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
def findByOCIRecipe(cls, recipe, statuses=None, job_ids=None):
conditions = [
OCIRecipeJob.recipe == recipe,
- OCIRecipeJob.job_type == cls.class_job_type]
+ OCIRecipeJob.job_type == cls.class_job_type,
+ ]
if statuses is not None:
conditions.append(Job._status.is_in(statuses))
if job_ids is not None:
conditions.append(OCIRecipeJob.job_id.is_in(job_ids))
- oci_jobs = IStore(OCIRecipeJob).find(
- OCIRecipeJob,
- OCIRecipeJob.job_id == Job.id,
- *conditions).order_by(Desc(OCIRecipeJob.job_id))
+ oci_jobs = (
+ IStore(OCIRecipeJob)
+ .find(OCIRecipeJob, OCIRecipeJob.job_id == Job.id, *conditions)
+ .order_by(Desc(OCIRecipeJob.job_id))
+ )
def preload_jobs(rows):
load_related(Job, rows, ["job_id"])
return DecoratedResultSet(
- oci_jobs, lambda oci_job: cls(oci_job), pre_iter_hook=preload_jobs)
+ oci_jobs, lambda oci_job: cls(oci_job), pre_iter_hook=preload_jobs
+ )
def getOperationDescription(self):
return "requesting builds of %s" % self.recipe
@@ -258,10 +260,15 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
# Sort this by architecture/processor name, so it's consistent
# when displayed
if build_ids:
- return IStore(OCIRecipeBuild).find(
- OCIRecipeBuild, OCIRecipeBuild.id.is_in(build_ids),
- OCIRecipeBuild.processor_id == Processor.id).order_by(
- Desc(Processor.name))
+ return (
+ IStore(OCIRecipeBuild)
+ .find(
+ OCIRecipeBuild,
+ OCIRecipeBuild.id.is_in(build_ids),
+ OCIRecipeBuild.processor_id == Processor.id,
+ )
+ .order_by(Desc(Processor.name))
+ )
else:
return EmptyResultSet()
@@ -280,7 +287,9 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
return {
# Converts keys to integer since saving json to database
# converts them to strings.
- int(k): v for k, v in self.metadata["uploaded_manifests"].items()}
+ int(k): v
+ for k, v in self.metadata["uploaded_manifests"].items()
+ }
def addUploadedManifest(self, build_id, manifest_info):
self.metadata["uploaded_manifests"][int(build_id)] = manifest_info
@@ -290,8 +299,8 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
builds = self.builds
# This just returns a dict, but Zope is really helpful here
status = removeSecurityProxy(
- getUtility(IOCIRecipeSet).getStatusSummaryForBuilds(
- list(builds)))
+ getUtility(IOCIRecipeSet).getStatusSummaryForBuilds(list(builds))
+ )
# This has a really long name!
singleStatus = OCIRecipeBuildRegistryUploadStatus
@@ -299,13 +308,16 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
# Set the pending upload status if either we're not done uploading,
# or there was no upload requested in the first place (no push rules)
- if status['status'] == BuildSetStatus.FULLYBUILT:
+ if status["status"] == BuildSetStatus.FULLYBUILT:
upload_status = [
- (x.registry_upload_status == singleStatus.UPLOADED or
- x.registry_upload_status == singleStatus.UNSCHEDULED)
- for x in status['builds']]
+ (
+ x.registry_upload_status == singleStatus.UPLOADED
+ or x.registry_upload_status == singleStatus.UNSCHEDULED
+ )
+ for x in status["builds"]
+ ]
if not all(upload_status):
- status['status'] = BuildSetStatus.FULLYBUILT_PENDING
+ status["status"] = BuildSetStatus.FULLYBUILT_PENDING
# Are we expecting an upload to be or to have been attempted?
# This is slightly complicated as the upload depends on the push
@@ -318,36 +330,38 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
# If all of the builds haven't finished, but the recipe currently
# has push rules specified, then we will attempt an upload
# in the future
- if any(not x.date_finished and x.recipe.can_upload_to_registry
- for x in builds):
+ if any(
+ not x.date_finished and x.recipe.can_upload_to_registry
+ for x in builds
+ ):
upload_requested = True
- status['upload_requested'] = upload_requested
+ status["upload_requested"] = upload_requested
# Convert the set of registry statuses into a single line
# for display
upload_status = [x.registry_upload_status for x in builds]
# Any of the builds failed
if any(x == singleStatus.FAILEDTOUPLOAD for x in upload_status):
- status['upload'] = setStatus.FAILEDTOUPLOAD
+ status["upload"] = setStatus.FAILEDTOUPLOAD
# All of the builds uploaded
elif all(x == singleStatus.UPLOADED for x in upload_status):
- status['upload'] = setStatus.UPLOADED
+ status["upload"] = setStatus.UPLOADED
# All of the builds are yet to attempt an upload
elif all(x == singleStatus.UNSCHEDULED for x in upload_status):
- status['upload'] = setStatus.UNSCHEDULED
+ status["upload"] = setStatus.UNSCHEDULED
# Any of the builds have uploaded. Set after 'all of the builds'
# have uploaded.
elif any(x == singleStatus.UPLOADED for x in upload_status):
- status['upload'] = setStatus.PARTIAL
+ status["upload"] = setStatus.PARTIAL
# And if it's none of the above, we're waiting
else:
- status['upload'] = setStatus.PENDING
+ status["upload"] = setStatus.PENDING
# Get the longest date and whether any of them are estimated
# for the summary of the builds
dates = [x.date for x in self.builds if x.date]
- status['date'] = max(dates) if dates else None
- status['date_estimated'] = any(x.estimate for x in self.builds)
+ status["date"] = max(dates) if dates else None
+ status["date_estimated"] = any(x.estimate for x in self.builds)
return status
@@ -356,12 +370,15 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
requester = self.requester
if requester is None:
log.info(
- "Skipping %r because the requester has been deleted." % self)
+ "Skipping %r because the requester has been deleted." % self
+ )
return
try:
self.builds = self.recipe.requestBuildsFromJob(
- requester, build_request=self.build_request,
- architectures=self.architectures)
+ requester,
+ build_request=self.build_request,
+ architectures=self.architectures,
+ )
self.error_message = None
except self.retry_error_types:
raise
diff --git a/lib/lp/oci/model/ocirecipesubscription.py b/lib/lp/oci/model/ocirecipesubscription.py
index 4de765e..ba657cf 100644
--- a/lib/lp/oci/model/ocirecipesubscription.py
+++ b/lib/lp/oci/model/ocirecipesubscription.py
@@ -3,15 +3,10 @@
"""OCIRecipe subscription model."""
-__all__ = [
- 'OCIRecipeSubscription'
-]
+__all__ = ["OCIRecipeSubscription"]
import pytz
-from storm.properties import (
- DateTime,
- Int,
- )
+from storm.properties import DateTime, Int
from storm.references import Reference
from zope.interface import implementer
@@ -26,12 +21,11 @@ from lp.services.database.stormbase import StormBase
class OCIRecipeSubscription(StormBase):
"""A relationship between a person and an OCI recipe."""
- __storm_table__ = 'OCIRecipeSubscription'
+ __storm_table__ = "OCIRecipeSubscription"
id = Int(primary=True)
- person_id = Int(
- "person", allow_none=False, validator=validate_person)
+ person_id = Int("person", allow_none=False, validator=validate_person)
person = Reference(person_id, "Person.id")
recipe_id = Int("recipe", allow_none=False)
@@ -40,7 +34,8 @@ class OCIRecipeSubscription(StormBase):
date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC)
subscribed_by_id = Int(
- "subscribed_by", allow_none=False, validator=validate_person)
+ "subscribed_by", allow_none=False, validator=validate_person
+ )
subscribed_by = Reference(subscribed_by_id, "Person.id")
def __init__(self, recipe, person, subscribed_by):
@@ -53,7 +48,9 @@ class OCIRecipeSubscription(StormBase):
"""See `IOCIRecipeSubscription`."""
if user is None:
return False
- return (user.inTeam(self.recipe.owner) or
- user.inTeam(self.person) or
- user.inTeam(self.subscribed_by) or
- IPersonRoles(user).in_admin)
+ return (
+ user.inTeam(self.recipe.owner)
+ or user.inTeam(self.person)
+ or user.inTeam(self.subscribed_by)
+ or IPersonRoles(user).in_admin
+ )
diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
index 1b71e33..4316efb 100644
--- a/lib/lp/oci/model/ociregistryclient.py
+++ b/lib/lp/oci/model/ociregistryclient.py
@@ -3,27 +3,22 @@
"""Client for talking to an OCI registry."""
-__all__ = [
- 'OCIRegistryClient'
-]
+__all__ = ["OCIRegistryClient"]
import base64
-from functools import partial
import hashlib
-from http.client import IncompleteRead
-from io import BytesIO
import json
import logging
import re
import tarfile
+from functools import partial
+from http.client import IncompleteRead
+from io import BytesIO
from urllib.parse import urlparse
import boto3
from botocore.config import Config
-from requests.exceptions import (
- ConnectionError,
- HTTPError,
- )
+from requests.exceptions import ConnectionError, HTTPError
from requests.utils import parse_dict_header
from tenacity import (
before_log,
@@ -31,7 +26,7 @@ from tenacity import (
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
- )
+)
from zope.interface import implementer
from lp.buildmaster.enums import BuildStatus
@@ -40,21 +35,20 @@ from lp.oci.interfaces.ociregistryclient import (
IOCIRegistryClient,
ManifestUploadFailed,
MultipleOCIRegistryError,
- )
+)
from lp.services.config import config
from lp.services.features import getFeatureFlag
from lp.services.librarian.utils import EncodableLibraryFileAlias
from lp.services.propertycache import cachedproperty
from lp.services.timeout import urlfetch
-
log = logging.getLogger(__name__)
# Helper function to call urlfetch(use_proxy=True, *args, **kwargs)
proxy_urlfetch = partial(urlfetch, use_proxy=True)
-OCI_AWS_BEARER_TOKEN_DOMAINS_FLAG = 'oci.push.aws.bearer_token_domains'
+OCI_AWS_BEARER_TOKEN_DOMAINS_FLAG = "oci.push.aws.bearer_token_domains"
def is_aws_bearer_token_domain(domain):
@@ -64,13 +58,12 @@ def is_aws_bearer_token_domain(domain):
if not domains:
# We know that public ECR default domain is bearer token. If the
# flag is not set, force it.
- domains = 'public.ecr.aws'
+ domains = "public.ecr.aws"
return any(domain.endswith(i) for i in domains.split())
@implementer(IOCIRegistryClient)
class OCIRegistryClient:
-
@classmethod
def _getJSONfile(cls, reference):
"""Read JSON out of a `LibraryFileAlias`."""
@@ -99,9 +92,12 @@ class OCIRegistryClient:
wait=wait_fixed(3),
before=before_log(log, logging.INFO),
reraise=True,
- retry=(retry_if_exception_type(ConnectionError) |
- retry_if_exception_type(IncompleteRead)),
- stop=stop_after_attempt(5))
+ retry=(
+ retry_if_exception_type(ConnectionError)
+ | retry_if_exception_type(IncompleteRead)
+ ),
+ stop=stop_after_attempt(5),
+ )
def _upload(cls, digest, push_rule, fileobj, length, http_client):
"""Upload a blob to the registry, using a given digest.
@@ -115,8 +111,8 @@ class OCIRegistryClient:
# Check if it already exists
try:
head_response = http_client.requestPath(
- "/blobs/{}".format(digest),
- method="HEAD")
+ "/blobs/{}".format(digest), method="HEAD"
+ )
if head_response.status_code == 200:
log.info("{} already found".format(digest))
return
@@ -126,7 +122,8 @@ class OCIRegistryClient:
raise http_error
post_response = http_client.requestPath(
- "/blobs/uploads/", method="POST")
+ "/blobs/uploads/", method="POST"
+ )
post_location = post_response.headers["Location"]
query_parsed = {"digest": digest}
@@ -137,19 +134,23 @@ class OCIRegistryClient:
params=query_parsed,
data=fileobj,
headers={"Content-Length": str(length)},
- method="PUT")
+ method="PUT",
+ )
except HTTPError as http_error:
put_response = http_error.response
if put_response.status_code != 201:
raise cls._makeRegistryError(
BlobUploadFailed,
"Upload of {} for {} failed".format(
- digest, push_rule.image_name),
- put_response)
+ digest, push_rule.image_name
+ ),
+ put_response,
+ )
@classmethod
- def _upload_layer(cls, digest, push_rule, lfa, http_client,
- upload_layers_uncompressed):
+ def _upload_layer(
+ cls, digest, push_rule, lfa, http_client, upload_layers_uncompressed
+ ):
"""Upload a layer blob to the registry.
Uses _upload, but opens the LFA and extracts the necessary files
@@ -162,9 +163,9 @@ class OCIRegistryClient:
lfa.open()
try:
if upload_layers_uncompressed:
- with tarfile.open(fileobj=lfa, mode='r|gz') as un_zipped:
+ with tarfile.open(fileobj=lfa, mode="r|gz") as un_zipped:
for tarinfo in un_zipped:
- if tarinfo.name != 'layer.tar':
+ if tarinfo.name != "layer.tar":
continue
fileobj = un_zipped.extractfile(tarinfo)
# XXX Work around requests handling of objects that
@@ -173,25 +174,34 @@ class OCIRegistryClient:
fileobj.len = tarinfo.size
try:
cls._upload(
- digest, push_rule, fileobj, tarinfo.size,
- http_client)
+ digest,
+ push_rule,
+ fileobj,
+ tarinfo.size,
+ http_client,
+ )
finally:
fileobj.close()
return tarinfo.size
else:
size = lfa.content.filesize
wrapper = EncodableLibraryFileAlias(lfa)
- cls._upload(
- digest, push_rule, wrapper, size,
- http_client)
+ cls._upload(digest, push_rule, wrapper, size, http_client)
return size
finally:
lfa.close()
@classmethod
- def _build_registry_manifest(cls, digests, config, config_json,
- config_sha, preloaded_data, layer_sizes,
- upload_layers_uncompressed):
+ def _build_registry_manifest(
+ cls,
+ digests,
+ config,
+ config_json,
+ config_sha,
+ preloaded_data,
+ layer_sizes,
+ upload_layers_uncompressed,
+ ):
"""Create an image manifest for the uploading image.
This involves nearly everything as digests and lengths are required.
@@ -206,14 +216,16 @@ class OCIRegistryClient:
# Create the initial manifest data with empty layer information
manifest = {
"schemaVersion": 2,
- "mediaType":
- "application/vnd.docker.distribution.manifest.v2+json",
+ "mediaType": (
+ "application/vnd.docker.distribution.manifest.v2+json"
+ ),
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": len(config_json),
"digest": "sha256:{}".format(config_sha),
},
- "layers": []}
+ "layers": [],
+ }
# Fill in the layer information
# For uploading the compressed tar.gz for each layer we need
@@ -224,13 +236,19 @@ class OCIRegistryClient:
digest = unzipped_sha
else:
digest = "sha256:{}".format(digests[unzipped_sha]["digest"])
- log.info("Adding digest: %s for layer %s to manifest file." %
- (digest, digests[unzipped_sha]["layer_id"]))
- manifest["layers"].append({
- "mediaType":
- "application/vnd.docker.image.rootfs.diff.tar.gzip",
- "size": layer_sizes[digest],
- "digest": digest})
+ log.info(
+ "Adding digest: %s for layer %s to manifest file."
+ % (digest, digests[unzipped_sha]["layer_id"])
+ )
+ manifest["layers"].append(
+ {
+ "mediaType": (
+ "application/vnd.docker.image.rootfs.diff.tar.gzip"
+ ),
+ "size": layer_sizes[digest],
+ "digest": digest,
+ }
+ )
log.info("LP constructed the following manifest: %s", manifest)
return manifest
@@ -246,8 +264,7 @@ class OCIRegistryClient:
data = {}
for section in manifest:
# Load the matching config file for this section
- config = cls._getJSONfile(
- build.getFileByName(section['Config']))
+ config = cls._getJSONfile(build.getFileByName(section["Config"]))
files = {"config_file": config}
for diff_id in config["rootfs"]["diff_ids"]:
# We may have already seen this diff ID.
@@ -274,10 +291,10 @@ class OCIRegistryClient:
if recipe.is_valid_branch_format:
ref_name = recipe.git_ref.path
# lp:1921865, account for tags in the correct format
- if ref_name.startswith('refs/tags/'):
- ref_name = ref_name[len('refs/tags/'):]
- elif ref_name.startswith('refs/heads/'):
- ref_name = ref_name[len('refs/heads/'):]
+ if ref_name.startswith("refs/tags/"):
+ ref_name = ref_name[len("refs/tags/") :]
+ elif ref_name.startswith("refs/heads/"):
+ ref_name = ref_name[len("refs/heads/") :]
tags.append("{}_{}".format(ref_name, "edge"))
else:
tags.append("edge")
@@ -291,12 +308,14 @@ class OCIRegistryClient:
url = "/manifests/{}".format(tag)
accept = "application/vnd.docker.distribution.manifest.list.v2+json"
response = http_client.requestPath(
- url, method="GET", headers={"Accept": accept})
+ url, method="GET", headers={"Accept": accept}
+ )
return response.json()
@classmethod
- def _uploadRegistryManifest(cls, http_client, registry_manifest,
- push_rule, tag, build=None):
+ def _uploadRegistryManifest(
+ cls, http_client, registry_manifest, push_rule, tag, build=None
+ ):
"""Uploads the build manifest, returning its content information.
The returned information can be used to create a Manifest list
@@ -314,27 +333,30 @@ class OCIRegistryClient:
tag = "sha256:{}".format(hashlib.sha256(data).hexdigest())
size = len(data)
content_type = registry_manifest.get(
- "mediaType",
- "application/vnd.docker.distribution.manifest.v2+json")
+ "mediaType", "application/vnd.docker.distribution.manifest.v2+json"
+ )
try:
manifest_response = http_client.requestPath(
"/manifests/{}".format(tag),
data=data,
headers={"Content-Type": content_type},
- method="PUT")
+ method="PUT",
+ )
digest = manifest_response.headers.get("Docker-Content-Digest")
except HTTPError as http_error:
manifest_response = http_error.response
if manifest_response.status_code != 201:
if build:
msg = "Failed to upload manifest for {} ({}) in {}".format(
- build.recipe.name, push_rule.registry_url, build.id)
+ build.recipe.name, push_rule.registry_url, build.id
+ )
else:
- msg = ("Failed to upload manifest of manifests for"
- " {} ({})").format(
- push_rule.recipe.name, push_rule.registry_url)
+ msg = (
+ "Failed to upload manifest of manifests for" " {} ({})"
+ ).format(push_rule.recipe.name, push_rule.registry_url)
raise cls._makeRegistryError(
- ManifestUploadFailed, msg, manifest_response)
+ ManifestUploadFailed, msg, manifest_response
+ )
return {"digest": digest, "size": size}
@classmethod
@@ -360,9 +382,9 @@ class OCIRegistryClient:
# not been requested to be uploaded to registries yet.
try:
lfa.open()
- with tarfile.open(fileobj=lfa, mode='r|gz') as un_zipped:
+ with tarfile.open(fileobj=lfa, mode="r|gz") as un_zipped:
for tarinfo in un_zipped:
- if tarinfo.name == 'layer.tar':
+ if tarinfo.name == "layer.tar":
return True
return False
finally:
@@ -370,8 +392,8 @@ class OCIRegistryClient:
@classmethod
def _upload_to_push_rule(
- cls, push_rule, build, manifest, digests, preloaded_data,
- tag=None):
+ cls, push_rule, build, manifest, digests, preloaded_data, tag=None
+ ):
http_client = RegistryHTTPClient.getInstance(push_rule)
for section in manifest:
@@ -385,20 +407,23 @@ class OCIRegistryClient:
first_id = config["rootfs"]["diff_ids"][0]
lfa = file_data[first_id]
upload_layers_uncompressed = cls.should_upload_layers_uncompressed(
- lfa)
+ lfa
+ )
for diff_id in config["rootfs"]["diff_ids"]:
if upload_layers_uncompressed:
digest = diff_id
else:
digest = "sha256:{}".format(
- file_data[diff_id].content.sha256)
+ file_data[diff_id].content.sha256
+ )
layer_size = cls._upload_layer(
digest,
push_rule,
file_data[diff_id],
http_client,
- upload_layers_uncompressed)
+ upload_layers_uncompressed,
+ )
layer_sizes[digest] = layer_size
# The config file is required in different forms, so we can
# calculate the sha, work these out and upload
@@ -409,18 +434,25 @@ class OCIRegistryClient:
push_rule,
BytesIO(config_json),
len(config_json),
- http_client)
+ http_client,
+ )
# Build the registry manifest from the image manifest
# and associated configs
registry_manifest = cls._build_registry_manifest(
- digests, config, config_json, config_sha,
+ digests,
+ config,
+ config_json,
+ config_sha,
preloaded_data[section["Config"]],
- layer_sizes, upload_layers_uncompressed)
+ layer_sizes,
+ upload_layers_uncompressed,
+ )
# Upload the registry manifest
manifest = cls._uploadRegistryManifest(
- http_client, registry_manifest, push_rule, tag, build)
+ http_client, registry_manifest, push_rule, tag, build
+ )
# Save the uploaded manifest location, so we can use it in case
# this is a multi-arch image upload.
@@ -453,8 +485,13 @@ class OCIRegistryClient:
for push_rule in build.recipe.push_rules:
try:
cls._upload_to_push_rule(
- push_rule, build, manifest, digests,
- preloaded_data, tag=None)
+ push_rule,
+ build,
+ manifest,
+ digests,
+ preloaded_data,
+ tag=None,
+ )
except Exception as e:
exceptions.append(e)
if len(exceptions) == 1:
@@ -473,7 +510,7 @@ class OCIRegistryClient:
"ppc64el": {"os": "linux", "architecture": "ppc64le"},
"riscv64": {"os": "linux", "architecture": "riscv64"},
"s390x": {"os": "linux", "architecture": "s390x"},
- }
+ }
@classmethod
def _makePlatformSpecifiers(cls, arch):
@@ -499,14 +536,16 @@ class OCIRegistryClient:
return platforms
@classmethod
- def makeMultiArchManifest(cls, http_client, push_rule, build_request,
- uploaded_builds, tag):
+ def makeMultiArchManifest(
+ cls, http_client, push_rule, build_request, uploaded_builds, tag
+ ):
"""Returns the multi-arch manifest content including all uploaded
builds of the given build_request.
"""
try:
current_manifest = cls._getCurrentRegistryManifest(
- http_client, tag)
+ http_client, tag
+ )
# Check if the current manifest is not an incompatible version.
version = current_manifest.get("schemaVersion", 1)
if version < 2 or "manifests" not in current_manifest:
@@ -519,23 +558,29 @@ class OCIRegistryClient:
current_manifest = None
msg_tpl = (
"No multi-arch manifest on registry %s (image name: %s). "
- "Uploading a new one.")
- log.info(msg_tpl % (
- push_rule.registry_url, push_rule.image_name))
+ "Uploading a new one."
+ )
+ log.info(
+ msg_tpl % (push_rule.registry_url, push_rule.image_name)
+ )
else:
raise
if current_manifest is None:
current_manifest = {
"schemaVersion": 2,
- "mediaType": ("application/"
- "vnd.docker.distribution.manifest.list.v2+json"),
- "manifests": []}
+ "mediaType": (
+ "application/"
+ "vnd.docker.distribution.manifest.list.v2+json"
+ ),
+ "manifests": [],
+ }
manifests = current_manifest["manifests"]
for build in uploaded_builds:
build_manifest = build_request.uploaded_manifests.get(build.id)
if not build_manifest:
log.info(
- "No build manifest found for build {}".format(build.id))
+ "No build manifest found for build {}".format(build.id)
+ )
continue
log.info("Build manifest found for build {}".format(build.id))
digest = build_manifest["digest"]
@@ -544,14 +589,18 @@ class OCIRegistryClient:
platforms = cls._makePlatformSpecifiers(arch)
manifest = next(
- (m for m in manifests if m["platform"] in platforms), None)
+ (m for m in manifests if m["platform"] in platforms), None
+ )
if manifest is None:
log.info(
"Appending multi-arch manifest for build {} "
- "with arch {}".format(build.id, arch))
+ "with arch {}".format(build.id, arch)
+ )
manifest = {
- "mediaType": ("application/"
- "vnd.docker.distribution.manifest.v2+json"),
+ "mediaType": (
+ "application/"
+ "vnd.docker.distribution.manifest.v2+json"
+ ),
"size": size,
"digest": digest,
"platform": platforms[0],
@@ -560,7 +609,8 @@ class OCIRegistryClient:
else:
log.info(
"Updating multi-arch manifest for build {} "
- "with arch {}".format(build.id, arch))
+ "with arch {}".format(build.id, arch)
+ )
manifest["digest"] = digest
manifest["size"] = size
manifest["platform"] = platforms[0]
@@ -577,10 +627,11 @@ class OCIRegistryClient:
if build.status == BuildStatus.SUPERSEDED:
return
if build.hasMoreRecentBuild():
- force_transition = (build.status == BuildStatus.FULLYBUILT)
+ force_transition = build.status == BuildStatus.FULLYBUILT
build.updateStatus(
BuildStatus.SUPERSEDED,
- force_invalid_transition=force_transition)
+ force_invalid_transition=force_transition,
+ )
@classmethod
def uploadManifestList(cls, build_request, uploaded_builds):
@@ -591,8 +642,11 @@ class OCIRegistryClient:
# manifest files were not superseded by newer builds.
for build in uploaded_builds:
cls.updateSupersededBuilds(build)
- uploaded_builds = [build for build in uploaded_builds
- if build.status != BuildStatus.SUPERSEDED]
+ uploaded_builds = [
+ build
+ for build in uploaded_builds
+ if build.status != BuildStatus.SUPERSEDED
+ ]
if not uploaded_builds:
return
for push_rule in build_request.recipe.push_rules:
@@ -600,15 +654,24 @@ class OCIRegistryClient:
try:
http_client = RegistryHTTPClient.getInstance(push_rule)
multi_manifest_content = cls.makeMultiArchManifest(
- http_client, push_rule, build_request, uploaded_builds,
- tag)
+ http_client,
+ push_rule,
+ build_request,
+ uploaded_builds,
+ tag,
+ )
cls._uploadRegistryManifest(
- http_client, multi_manifest_content, push_rule, tag,
- build=None)
+ http_client,
+ multi_manifest_content,
+ push_rule,
+ tag,
+ build=None,
+ )
except Exception:
log.exception(
"Exception in uploading manifest for OCI build "
- "request {} with tag {}".format(build_request.id, tag))
+ "request {} with tag {}".format(build_request.id, tag)
+ )
raise
@@ -626,8 +689,8 @@ class RegistryHTTPClient:
def credentials(self):
"""Returns a tuple of (username, password)."""
auth = self.push_rule.registry_credentials.getCredentials()
- if auth.get('username'):
- return auth['username'], auth.get('password')
+ if auth.get("username"):
+ return auth["username"], auth.get("password")
return None, None
@property
@@ -664,10 +727,12 @@ class RegistryHTTPClient:
# If we got back an "UNAUTHORIZED" error with "Www-Authenticate"
# header, we should check what type of authorization we should use.
header_key = "Www-Authenticate"
- if (e.response.status_code == 401
- and header_key in e.response.headers):
- auth_type = e.response.headers[header_key].split(' ', 1)[0]
- if auth_type == 'Bearer':
+ if (
+ e.response.status_code == 401
+ and header_key in e.response.headers
+ ):
+ auth_type = e.response.headers[header_key].split(" ", 1)[0]
+ if auth_type == "Bearer":
# Note that, although we have the realm where to
# authenticate, we do not retrieve the authentication
# token here. Different operations might need different
@@ -676,11 +741,13 @@ class RegistryHTTPClient:
# are actually doing the operations and we will get info
# about what scope we will need.
return BearerTokenRegistryClient(push_rule)
- elif auth_type == 'Basic':
+ elif auth_type == "Basic":
return RegistryHTTPClient(push_rule)
raise OCIRegistryAuthenticationError(
- "Unknown authentication type for %s registry" %
- push_rule.registry_url, e)
+ "Unknown authentication type for %s registry"
+ % push_rule.registry_url,
+ e,
+ )
class BearerTokenRegistryClient(RegistryHTTPClient):
@@ -702,8 +769,8 @@ class BearerTokenRegistryClient(RegistryHTTPClient):
This method parses the appropriate header from the request and returns
the token type and the key-value pairs that should be used as query
parameters of the token GET request."""
- instructions = request.headers['Www-Authenticate']
- token_type, values = instructions.split(' ', 1)
+ instructions = request.headers["Www-Authenticate"]
+ token_type, values = instructions.split(" ", 1)
dict_values = parse_dict_header(values)
return token_type, dict_values
@@ -716,17 +783,20 @@ class BearerTokenRegistryClient(RegistryHTTPClient):
except KeyError:
raise OCIRegistryAuthenticationError(
"Auth instructions didn't include realm to get the token: %s"
- % values)
+ % values
+ )
# We should use the basic auth version for this request.
response = super().request(
- url, params=values, method="GET", auth=self.credentials)
+ url, params=values, method="GET", auth=self.credentials
+ )
response.raise_for_status()
response_data = response.json()
try:
self.auth_token = response_data["token"]
except KeyError:
raise OCIRegistryAuthenticationError(
- "Could not get token from response data: %s" % response_data)
+ "Could not get token from response data: %s" % response_data
+ )
def request(self, url, auth_retry=True, *args, **request_kwargs):
"""Does a request, handling authentication cycle in case of 401
@@ -743,8 +813,12 @@ class BearerTokenRegistryClient(RegistryHTTPClient):
if auth_retry and e.response.status_code == 401:
self.authenticate(e.response)
return self.request(
- url, auth_retry=False, headers=headers,
- *args, **request_kwargs)
+ url,
+ auth_retry=False,
+ headers=headers,
+ *args,
+ **request_kwargs,
+ )
raise
@@ -756,23 +830,28 @@ class AWSAuthenticatorMixin:
def _getClientParameters(self):
if config.launchpad.http_proxy:
- boto_config = Config(proxies={
- 'http': config.launchpad.http_proxy,
- 'https': config.launchpad.http_proxy})
+ boto_config = Config(
+ proxies={
+ "http": config.launchpad.http_proxy,
+ "https": config.launchpad.http_proxy,
+ }
+ )
else:
boto_config = Config()
auth = self.push_rule.registry_credentials.getCredentials()
- username, password = auth['username'], auth.get('password')
+ username, password = auth["username"], auth.get("password")
region = self._getRegion()
log.info("Trying to authenticate with AWS in region %s" % region)
return dict(
aws_access_key_id=username,
- aws_secret_access_key=password, region_name=region,
- config=boto_config)
+ aws_secret_access_key=password,
+ region_name=region,
+ config=boto_config,
+ )
def _getBotoClient(self):
params = self._getClientParameters()
- client_type = 'ecr-public' if self.is_public_ecr else 'ecr'
+ client_type = "ecr-public" if self.is_public_ecr else "ecr"
return boto3.client(client_type, **params)
@property
@@ -792,7 +871,7 @@ class AWSAuthenticatorMixin:
# Try to guess from the domain. The format should be something like
# 'xxx.dkr.ecr.sa-east-1.amazonaws.com'. 'sa-east-1' is the region.
domain = urlparse(self.push_rule.registry_url).netloc
- if re.match(r'.+\.dkr\.ecr\..+\.amazonaws\.com', domain):
+ if re.match(r".+\.dkr\.ecr\..+\.amazonaws\.com", domain):
return domain.split(".")[-3]
raise OCIRegistryAuthenticationError("Unknown AWS region.")
@@ -809,25 +888,31 @@ class AWSAuthenticatorMixin:
# both situations.
if isinstance(auth_data, list):
auth_data = auth_data[0]
- authorization_token = auth_data['authorizationToken']
- username, password = base64.b64decode(
- authorization_token).decode().split(':')
+ authorization_token = auth_data["authorizationToken"]
+ username, password = (
+ base64.b64decode(authorization_token).decode().split(":")
+ )
return username, password
except Exception as e:
- log.error("Error trying to get authorization token for ECR "
- "registry: %s(%s)" % (e.__class__, e))
+ log.error(
+ "Error trying to get authorization token for ECR "
+ "registry: %s(%s)" % (e.__class__, e)
+ )
raise OCIRegistryAuthenticationError(
- "It was not possible to get AWS credentials for %s: %s" %
- (self.push_rule.registry_url, e))
+ "It was not possible to get AWS credentials for %s: %s"
+ % (self.push_rule.registry_url, e)
+ )
class AWSRegistryHTTPClient(AWSAuthenticatorMixin, RegistryHTTPClient):
"""AWS registry client with authentication flow based on basic auth."""
+
pass
class AWSRegistryBearerTokenClient(
- AWSAuthenticatorMixin, BearerTokenRegistryClient):
- """AWS registry client with authentication flow based on bearer token flow.
- """
+ AWSAuthenticatorMixin, BearerTokenRegistryClient
+):
+ """AWS registry client with authentication flow based on bearer tokens."""
+
pass
diff --git a/lib/lp/oci/model/ociregistrycredentials.py b/lib/lp/oci/model/ociregistrycredentials.py
index 28be770..2a6684e 100644
--- a/lib/lp/oci/model/ociregistrycredentials.py
+++ b/lib/lp/oci/model/ociregistrycredentials.py
@@ -4,20 +4,15 @@
"""Registry credentials for use by an `OCIPushRule`."""
__all__ = [
- 'OCIRegistryCredentials',
- 'OCIRegistryCredentialsSet',
- ]
+ "OCIRegistryCredentials",
+ "OCIRegistryCredentialsSet",
+]
import base64
import json
from storm.databases.postgres import JSON
-from storm.locals import (
- Int,
- Reference,
- Storm,
- Unicode,
- )
+from storm.locals import Int, Reference, Storm, Unicode
from zope.component import getUtility
from zope.interface import implementer
from zope.schema import ValidationError
@@ -29,24 +24,21 @@ from lp.oci.interfaces.ociregistrycredentials import (
IOCIRegistryCredentialsSet,
OCIRegistryCredentialsAlreadyExist,
OCIRegistryCredentialsNotOwner,
- )
+)
from lp.services.config import config
-from lp.services.crypto.interfaces import (
- CryptoError,
- IEncryptedContainer,
- )
+from lp.services.crypto.interfaces import CryptoError, IEncryptedContainer
from lp.services.crypto.model import NaClEncryptedContainerBase
from lp.services.database.interfaces import IStore
@implementer(IEncryptedContainer)
class OCIRegistrySecretsEncryptedContainer(NaClEncryptedContainerBase):
-
@property
def public_key_bytes(self):
if config.oci.registry_secrets_public_key is not None:
return base64.b64decode(
- config.oci.registry_secrets_public_key.encode('UTF-8'))
+ config.oci.registry_secrets_public_key.encode("UTF-8")
+ )
else:
return None
@@ -54,7 +46,8 @@ class OCIRegistrySecretsEncryptedContainer(NaClEncryptedContainerBase):
def private_key_bytes(self):
if config.oci.registry_secrets_private_key is not None:
return base64.b64decode(
- config.oci.registry_secrets_private_key.encode('UTF-8'))
+ config.oci.registry_secrets_private_key.encode("UTF-8")
+ )
else:
return None
@@ -63,30 +56,36 @@ def url_validator(allowed_schemes):
def wrapped(obj, attr, value):
if not validate_url(value, allowed_schemes):
raise ValidationError(
- "%s is not a valid URL for '%s' attribute" % (value, attr))
+ "%s is not a valid URL for '%s' attribute" % (value, attr)
+ )
return value
+
return wrapped
@implementer(IOCIRegistryCredentials)
class OCIRegistryCredentials(Storm):
- __storm_table__ = 'OCIRegistryCredentials'
+ __storm_table__ = "OCIRegistryCredentials"
id = Int(primary=True)
- owner_id = Int(name='owner', allow_none=False)
- owner = Reference(owner_id, 'Person.id')
+ owner_id = Int(name="owner", allow_none=False)
+ owner = Reference(owner_id, "Person.id")
url = Unicode(
- name="url", allow_none=False, validator=url_validator(
- IOCIRegistryCredentials['url'].allowed_schemes))
+ name="url",
+ allow_none=False,
+ validator=url_validator(
+ IOCIRegistryCredentials["url"].allowed_schemes
+ ),
+ )
_credentials = JSON(name="credentials", allow_none=True)
# The list of dict keys that should not be encrypted when storing
# _credentials attribute.
- _UNENCRYPTED_CREDENTIALS_FIELDS = ['username', 'region']
+ _UNENCRYPTED_CREDENTIALS_FIELDS = ["username", "region"]
def __init__(self, owner, url, credentials):
self.owner = owner
@@ -97,8 +96,11 @@ class OCIRegistryCredentials(Storm):
container = getUtility(IEncryptedContainer, "oci-registry-secrets")
try:
data = dict(self._credentials or {})
- decrypted_data = json.loads(container.decrypt(
- self._credentials['credentials_encrypted']).decode("UTF-8"))
+ decrypted_data = json.loads(
+ container.decrypt(
+ self._credentials["credentials_encrypted"]
+ ).decode("UTF-8")
+ )
if decrypted_data:
data.update(decrypted_data)
data.pop("credentials_encrypted")
@@ -119,7 +121,9 @@ class OCIRegistryCredentials(Storm):
# Encrypt the rest of the dict.
data = {
"credentials_encrypted": removeSecurityProxy(
- container.encrypt(json.dumps(copy).encode('UTF-8')))}
+ container.encrypt(json.dumps(copy).encode("UTF-8"))
+ )
+ }
# Put back the fields that shouldn't be encrypted.
for field in self._UNENCRYPTED_CREDENTIALS_FIELDS:
value = unencrypted_fields[field]
@@ -129,19 +133,19 @@ class OCIRegistryCredentials(Storm):
@property
def username(self):
- return self._credentials.get('username')
+ return self._credentials.get("username")
@username.setter
def username(self, value):
- self._credentials['username'] = value
+ self._credentials["username"] = value
@property
def region(self):
- return self._credentials.get('region')
+ return self._credentials.get("region")
@region.setter
def region(self, value):
- self._credentials['region'] = value
+ self._credentials["region"] = value
def destroySelf(self):
"""See `IOCIRegistryCredentials`."""
@@ -150,23 +154,24 @@ class OCIRegistryCredentials(Storm):
@implementer(IOCIRegistryCredentialsSet)
class OCIRegistryCredentialsSet:
-
def _checkOwner(self, registrant, owner):
if not registrant.inTeam(owner):
if owner.is_team:
raise OCIRegistryCredentialsNotOwner(
- "%s is not a member of %s." %
- (registrant.display_name, owner.display_name))
+ "%s is not a member of %s."
+ % (registrant.display_name, owner.display_name)
+ )
else:
raise OCIRegistryCredentialsNotOwner(
- "%s cannot create credentials owned by %s." %
- (registrant.display_name, owner.display_name))
+ "%s cannot create credentials owned by %s."
+ % (registrant.display_name, owner.display_name)
+ )
def _checkForExisting(self, owner, url, credentials):
for existing in self.findByOwner(owner):
url_match = existing.url == url
- username_match = existing.username == credentials.get('username')
- region_match = existing.region == credentials.get('region')
+ username_match = existing.username == credentials.get("username")
+ region_match = existing.region == credentials.get("region")
if url_match and username_match and region_match:
return existing
return None
@@ -179,8 +184,9 @@ class OCIRegistryCredentialsSet:
raise OCIRegistryCredentialsAlreadyExist()
return OCIRegistryCredentials(owner, url, credentials)
- def getOrCreate(self, registrant, owner, url, credentials,
- override_owner=False):
+ def getOrCreate(
+ self, registrant, owner, url, credentials, override_owner=False
+ ):
"""See `IOCIRegistryCredentialsSet`."""
if not override_owner:
self._checkOwner(registrant, owner)
@@ -188,11 +194,12 @@ class OCIRegistryCredentialsSet:
if existing:
return existing
return self.new(
- registrant, owner, url, credentials, override_owner=override_owner)
+ registrant, owner, url, credentials, override_owner=override_owner
+ )
def findByOwner(self, owner):
"""See `IOCIRegistryCredentialsSet`."""
store = IStore(OCIRegistryCredentials)
return store.find(
- OCIRegistryCredentials,
- OCIRegistryCredentials.owner == owner)
+ OCIRegistryCredentials, OCIRegistryCredentials.owner == owner
+ )
diff --git a/lib/lp/oci/security.py b/lib/lp/oci/security.py
index 0546859..d940124 100644
--- a/lib/lp/oci/security.py
+++ b/lib/lp/oci/security.py
@@ -9,12 +9,9 @@ from lp.app.security import (
AnonymousAuthorization,
AuthorizationBase,
DelegatedAuthorization,
- )
+)
from lp.oci.interfaces.ocipushrule import IOCIPushRule
-from lp.oci.interfaces.ocirecipe import (
- IOCIRecipe,
- IOCIRecipeBuildRequest,
- )
+from lp.oci.interfaces.ocirecipe import IOCIRecipe, IOCIRecipeBuildRequest
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.oci.interfaces.ocirecipesubscription import IOCIRecipeSubscription
from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentials
@@ -22,17 +19,18 @@ from lp.security import AdminByBuilddAdmin
class ViewOCIRecipeBuildRequest(DelegatedAuthorization):
- permission = 'launchpad.View'
+ permission = "launchpad.View"
usedfor = IOCIRecipeBuildRequest
def __init__(self, obj):
- super().__init__(obj, obj.recipe, 'launchpad.View')
+ super().__init__(obj, obj.recipe, "launchpad.View")
class ViewOCIRecipe(AnonymousAuthorization):
"""Anyone can view public `IOCIRecipe`, but only subscribers can view
private ones.
"""
+
usedfor = IOCIRecipe
def checkUnauthenticated(self):
@@ -43,13 +41,13 @@ class ViewOCIRecipe(AnonymousAuthorization):
class EditOCIRecipe(AuthorizationBase):
- permission = 'launchpad.Edit'
+ permission = "launchpad.Edit"
usedfor = IOCIRecipe
def checkAuthenticated(self, user):
return (
- user.isOwner(self.obj) or
- user.in_commercial_admin or user.in_admin)
+ user.isOwner(self.obj) or user.in_commercial_admin or user.in_admin
+ )
class AdminOCIRecipe(AuthorizationBase):
@@ -59,19 +57,20 @@ class AdminOCIRecipe(AuthorizationBase):
settings, so they can only be changed by "PPA"/commercial admins, or by
"PPA" self admins on OCI recipes that they can already edit.
"""
- permission = 'launchpad.Admin'
+
+ permission = "launchpad.Admin"
usedfor = IOCIRecipe
def checkAuthenticated(self, user):
if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
return True
- return (
- user.in_ppa_self_admins
- and EditOCIRecipe(self.obj).checkAuthenticated(user))
+ return user.in_ppa_self_admins and EditOCIRecipe(
+ self.obj
+ ).checkAuthenticated(user)
class OCIRecipeSubscriptionEdit(AuthorizationBase):
- permission = 'launchpad.Edit'
+ permission = "launchpad.Edit"
usedfor = IOCIRecipeSubscription
def checkAuthenticated(self, user):
@@ -84,14 +83,16 @@ class OCIRecipeSubscriptionEdit(AuthorizationBase):
the OCI recipe owner is a team, then members of the team can edit
the subscription.
"""
- return (user.inTeam(self.obj.recipe.owner) or
- user.inTeam(self.obj.person) or
- user.inTeam(self.obj.subscribed_by) or
- user.in_admin)
+ return (
+ user.inTeam(self.obj.recipe.owner)
+ or user.inTeam(self.obj.person)
+ or user.inTeam(self.obj.subscribed_by)
+ or user.in_admin
+ )
class OCIRecipeSubscriptionView(AuthorizationBase):
- permission = 'launchpad.View'
+ permission = "launchpad.View"
usedfor = IOCIRecipeSubscription
def checkUnauthenticated(self):
@@ -102,7 +103,7 @@ class OCIRecipeSubscriptionView(AuthorizationBase):
class ViewOCIRecipeBuild(DelegatedAuthorization):
- permission = 'launchpad.View'
+ permission = "launchpad.View"
usedfor = IOCIRecipeBuild
def iter_objects(self):
@@ -110,7 +111,7 @@ class ViewOCIRecipeBuild(DelegatedAuthorization):
class EditOCIRecipeBuild(AdminByBuilddAdmin):
- permission = 'launchpad.Edit'
+ permission = "launchpad.Edit"
usedfor = IOCIRecipeBuild
def checkAuthenticated(self, user):
@@ -131,27 +132,28 @@ class AdminOCIRecipeBuild(AdminByBuilddAdmin):
class ViewOCIRegistryCredentials(AuthorizationBase):
- permission = 'launchpad.View'
+ permission = "launchpad.View"
usedfor = IOCIRegistryCredentials
def checkAuthenticated(self, user):
# This must be kept in sync with user_can_edit_credentials_for_owner
# in lp.oci.interfaces.ociregistrycredentials.
- return (
- user.isOwner(self.obj) or
- user.in_admin)
+ return user.isOwner(self.obj) or user.in_admin
class ViewOCIPushRule(AnonymousAuthorization):
"""Anyone can view an `IOCIPushRule`."""
+
usedfor = IOCIPushRule
class OCIPushRuleEdit(AuthorizationBase):
- permission = 'launchpad.Edit'
+ permission = "launchpad.Edit"
usedfor = IOCIPushRule
def checkAuthenticated(self, user):
return (
- user.isOwner(self.obj.recipe) or
- user.in_commercial_admin or user.in_admin)
+ user.isOwner(self.obj.recipe)
+ or user.in_commercial_admin
+ or user.in_admin
+ )
diff --git a/lib/lp/oci/subscribers/ocirecipebuild.py b/lib/lp/oci/subscribers/ocirecipebuild.py
index 3394826..abdc541 100644
--- a/lib/lp/oci/subscribers/ocirecipebuild.py
+++ b/lib/lp/oci/subscribers/ocirecipebuild.py
@@ -21,12 +21,22 @@ def _trigger_oci_recipe_build_webhook(build, action):
payload = {
"recipe_build": canonical_url(build, force_local_path=True),
"action": action,
- }
- payload.update(compose_webhook_payload(
- IOCIRecipeBuild, build,
- ["recipe", "build_request", "status", "registry_upload_status"]))
+ }
+ payload.update(
+ compose_webhook_payload(
+ IOCIRecipeBuild,
+ build,
+ [
+ "recipe",
+ "build_request",
+ "status",
+ "registry_upload_status",
+ ],
+ )
+ )
getUtility(IWebhookSet).trigger(
- build.recipe, "oci-recipe:build:0.1", payload)
+ build.recipe, "oci-recipe:build:0.1", payload
+ )
def oci_recipe_build_created(build, event):
@@ -42,11 +52,14 @@ def oci_recipe_build_modified(build, event):
if status_changed or registry_changed:
_trigger_oci_recipe_build_webhook(build, "status-changed")
if status_changed:
- if (build.recipe.can_upload_to_registry and
- build.status == BuildStatus.FULLYBUILT):
+ if (
+ build.recipe.can_upload_to_registry
+ and build.status == BuildStatus.FULLYBUILT
+ ):
log.info("Scheduling upload of %r to registries." % build)
getUtility(IOCIRegistryUploadJobSource).create(build)
else:
log.info(
- "%r is not configured for upload to registries." %
- build.recipe)
+ "%r is not configured for upload to registries."
+ % build.recipe
+ )
diff --git a/lib/lp/oci/tests/helpers.py b/lib/lp/oci/tests/helpers.py
index 336b078..242f6cb 100644
--- a/lib/lp/oci/tests/helpers.py
+++ b/lib/lp/oci/tests/helpers.py
@@ -8,36 +8,39 @@ __all__ = []
import base64
from nacl.public import PrivateKey
-from testtools.matchers import (
- AfterPreprocessing,
- MatchesAll,
- )
+from testtools.matchers import AfterPreprocessing, MatchesAll
from zope.security.proxy import removeSecurityProxy
from lp.oci.interfaces.ocirecipe import (
OCI_RECIPE_ALLOW_CREATE,
OCI_RECIPE_PRIVATE_FEATURE_FLAG,
- )
+)
from lp.services.features.testing import FeatureFixture
class OCIConfigHelperMixin:
-
def setConfig(self, feature_flags=None):
self.private_key = PrivateKey.generate()
self.pushConfig(
"oci",
registry_secrets_public_key=base64.b64encode(
- bytes(self.private_key.public_key)).decode("UTF-8"))
+ bytes(self.private_key.public_key)
+ ).decode("UTF-8"),
+ )
self.pushConfig(
"oci",
registry_secrets_private_key=base64.b64encode(
- bytes(self.private_key)).decode("UTF-8"))
+ bytes(self.private_key)
+ ).decode("UTF-8"),
+ )
# Default feature flags for our tests
feature_flags = feature_flags or {}
- feature_flags.update({
- OCI_RECIPE_ALLOW_CREATE: 'on',
- OCI_RECIPE_PRIVATE_FEATURE_FLAG: 'on'})
+ feature_flags.update(
+ {
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ OCI_RECIPE_PRIVATE_FEATURE_FLAG: "on",
+ }
+ )
self.useFixture(FeatureFixture(feature_flags))
@@ -53,4 +56,6 @@ class MatchesOCIRegistryCredentials(MatchesAll):
main_matcher,
AfterPreprocessing(
lambda matchee: removeSecurityProxy(matchee).getCredentials(),
- credentials_matcher))
+ credentials_matcher,
+ ),
+ )
diff --git a/lib/lp/oci/tests/test_ocipushrule.py b/lib/lp/oci/tests/test_ocipushrule.py
index 043c6b9..aecdc7b 100644
--- a/lib/lp/oci/tests/test_ocipushrule.py
+++ b/lib/lp/oci/tests/test_ocipushrule.py
@@ -12,13 +12,10 @@ from lp.oci.interfaces.ocipushrule import (
IOCIPushRule,
IOCIPushRuleSet,
OCIPushRuleAlreadyExists,
- )
+)
from lp.oci.model.ociregistrycredentials import OCIRegistryCredentials
from lp.oci.tests.helpers import OCIConfigHelperMixin
-from lp.testing import (
- person_logged_in,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory, person_logged_in
from lp.testing.layers import LaunchpadZopelessLayer
@@ -37,25 +34,26 @@ class TestOCIPushRule(OCIConfigHelperMixin, TestCaseWithFactory):
def test_change_attribute(self):
push_rule = self.factory.makeOCIPushRule()
with person_logged_in(push_rule.recipe.owner):
- push_rule.setNewImageName('new image name')
+ push_rule.setNewImageName("new image name")
found_rule = push_rule.recipe.push_rules[0]
- self.assertEqual(found_rule.image_name, 'new image name')
+ self.assertEqual(found_rule.image_name, "new image name")
def test_change_image_name_existing(self):
first = self.factory.makeOCIPushRule(image_name="first")
second = self.factory.makeOCIPushRule(
image_name="second",
- registry_credentials=first.registry_credentials)
+ registry_credentials=first.registry_credentials,
+ )
self.assertRaises(
- OCIPushRuleAlreadyExists,
- second.setNewImageName,
- first.image_name)
+ OCIPushRuleAlreadyExists, second.setNewImageName, first.image_name
+ )
def test_username_retrieval(self):
credentials = self.factory.makeOCIRegistryCredentials()
push_rule = self.factory.makeOCIPushRule(
- registry_credentials=credentials)
+ registry_credentials=credentials
+ )
self.assertEqual(credentials.username, push_rule.username)
def test_valid_registry_url(self):
@@ -65,7 +63,11 @@ class TestOCIPushRule(OCIConfigHelperMixin, TestCaseWithFactory):
self.assertRaisesRegex(
ValidationError,
"asdf://foo.com is not a valid URL for 'url' attribute",
- OCIRegistryCredentials, owner, url, credentials)
+ OCIRegistryCredentials,
+ owner,
+ url,
+ credentials,
+ )
# Avoid trying to flush the incomplete object on cleanUp.
Store.of(owner).rollback()
@@ -89,14 +91,17 @@ class TestOCIPushRuleSet(OCIConfigHelperMixin, TestCaseWithFactory):
push_rule = getUtility(IOCIPushRuleSet).new(
recipe=recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
self.assertThat(
push_rule,
MatchesStructure.byEquality(
recipe=recipe,
registry_credentials=registry_credentials,
- image_name=image_name))
+ image_name=image_name,
+ ),
+ )
def test_new_with_existing(self):
recipe = self.factory.makeOCIRecipe()
@@ -105,9 +110,13 @@ class TestOCIPushRuleSet(OCIConfigHelperMixin, TestCaseWithFactory):
getUtility(IOCIPushRuleSet).new(
recipe=recipe,
registry_credentials=registry_credentials,
- image_name=image_name)
+ image_name=image_name,
+ )
self.assertRaises(
OCIPushRuleAlreadyExists,
getUtility(IOCIPushRuleSet).new,
- recipe, registry_credentials, image_name)
+ recipe,
+ registry_credentials,
+ image_name,
+ )
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index a697657..0262c88 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -3,9 +3,10 @@
"""Tests for OCI image building recipe functionality."""
-from datetime import datetime
import json
+from datetime import datetime
+import transaction
from fixtures import FakeLogger
from storm.exceptions import LostObjectError
from storm.store import Store
@@ -18,14 +19,10 @@ from testtools.matchers import (
MatchesDict,
MatchesSetwise,
MatchesStructure,
- )
-import transaction
+)
from zope.component import getUtility
from zope.schema import ValidationError
-from zope.security.interfaces import (
- ForbiddenAttribute,
- Unauthorized,
- )
+from zope.security.interfaces import ForbiddenAttribute, Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
@@ -35,53 +32,47 @@ from lp.code.tests.helpers import GitHostingFixture
from lp.oci.interfaces.ocipushrule import (
IOCIPushRuleSet,
OCIPushRuleAlreadyExists,
- )
+)
from lp.oci.interfaces.ocirecipe import (
+ OCI_RECIPE_ALLOW_CREATE,
+ OCI_RECIPE_BUILD_DISTRIBUTION,
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
CannotModifyOCIRecipeProcessor,
DuplicateOCIRecipeName,
IOCIRecipe,
IOCIRecipeSet,
NoSourceForOCIRecipe,
NoSuchOCIRecipe,
- OCI_RECIPE_ALLOW_CREATE,
- OCI_RECIPE_BUILD_DISTRIBUTION,
- OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
OCIRecipeBuildAlreadyPending,
OCIRecipeBuildRequestStatus,
OCIRecipeNotOwner,
OCIRecipePrivacyMismatch,
UsingDistributionCredentials,
- )
+)
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
from lp.oci.interfaces.ociregistrycredentials import (
OCIRegistryCredentialsNotOwner,
- )
+)
from lp.oci.tests.helpers import (
MatchesOCIRegistryCredentials,
OCIConfigHelperMixin,
- )
+)
from lp.registry.enums import (
BranchSharingPolicy,
PersonVisibility,
TeamMembershipPolicy,
- )
+)
from lp.registry.interfaces.accesspolicy import (
IAccessArtifactSource,
IAccessPolicyArtifactSource,
IAccessPolicySource,
- )
+)
from lp.registry.interfaces.ociproject import OCIProjectRecipeInvalid
from lp.registry.interfaces.series import SeriesStatus
-from lp.registry.model.accesspolicy import (
- AccessArtifact,
- AccessArtifactGrant,
- )
+from lp.registry.model.accesspolicy import AccessArtifact, AccessArtifactGrant
from lp.services.config import config
-from lp.services.database.constants import (
- ONE_DAY_AGO,
- UTC_NOW,
- )
+from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import flush_database_caches
from lp.services.features.testing import FeatureFixture
@@ -91,19 +82,16 @@ from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.snapshot import notify_modified
from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
+ StormStatementRecorder,
+ TestCaseWithFactory,
admin_logged_in,
api_url,
login_admin,
login_person,
person_logged_in,
- StormStatementRecorder,
- TestCaseWithFactory,
- )
+)
from lp.testing.dbuser import dbuser
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.pages import webservice_for_person
@@ -113,7 +101,7 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: "on"}))
def test_implements_interface(self):
target = self.factory.makeOCIRecipe()
@@ -125,7 +113,7 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
project = self.factory.makeProduct()
oci_project = self.factory.makeOCIProject(pillar=project)
recipe = self.factory.makeOCIRecipe(oci_project=oci_project)
- self.assertEqual('ubuntu', recipe.distribution.name)
+ self.assertEqual("ubuntu", recipe.distribution.name)
def test_feature_flag_distribution_on_project_pillar(self):
# With the OCI_RECIPE_BUILD_DISTRIBUTION feature flag set, we should
@@ -147,9 +135,11 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
with FeatureFixture({OCI_RECIPE_BUILD_DISTRIBUTION: "banana-distro"}):
expected_msg = (
"'banana-distro' is not a valid value for feature flag '%s'"
- % OCI_RECIPE_BUILD_DISTRIBUTION)
+ % OCI_RECIPE_BUILD_DISTRIBUTION
+ )
self.assertRaisesWithContent(
- ValueError, expected_msg, getattr, recipe, 'distribution')
+ ValueError, expected_msg, getattr, recipe, "distribution"
+ )
def test_distribution_for_distro_based_oci_project(self):
# For distribution-based OCI projects, we should use OCIProject's
@@ -171,81 +161,98 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
with notify_modified(removeSecurityProxy(recipe), ["name"]):
pass
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
def test_checkRequestBuild(self):
ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe())
unrelated_person = self.factory.makePerson()
self.assertRaises(
- OCIRecipeNotOwner,
- ocirecipe._checkRequestBuild,
- unrelated_person)
+ OCIRecipeNotOwner, ocirecipe._checkRequestBuild, unrelated_person
+ )
- def getDistroArchSeries(self, distroseries, proc_name="386",
- arch_tag="i386"):
+ def getDistroArchSeries(
+ self, distroseries, proc_name="386", arch_tag="i386"
+ ):
processor = getUtility(IProcessorSet).getByName(proc_name)
das = self.factory.makeDistroArchSeries(
- distroseries=distroseries, architecturetag=arch_tag,
- processor=processor)
+ distroseries=distroseries,
+ architecturetag=arch_tag,
+ processor=processor,
+ )
fake_chroot = self.factory.makeLibraryFileAlias(
- filename="fake_chroot.tar.gz", db_only=True)
+ filename="fake_chroot.tar.gz", db_only=True
+ )
das.addOrUpdateChroot(fake_chroot)
return das
def test_hasPendingBuilds(self):
ocirecipe = removeSecurityProxy(
- self.factory.makeOCIRecipe(require_virtualized=False))
+ self.factory.makeOCIRecipe(require_virtualized=False)
+ )
distro = ocirecipe.oci_project.distribution
series = self.factory.makeDistroSeries(
- distribution=distro, status=SeriesStatus.CURRENT)
+ distribution=distro, status=SeriesStatus.CURRENT
+ )
arch_series_386 = self.getDistroArchSeries(series, "386", "386")
arch_series_hppa = self.getDistroArchSeries(series, "hppa", "hppa")
# Successful build (i386)
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.FULLYBUILT,
- distro_arch_series=arch_series_386)
+ recipe=ocirecipe,
+ status=BuildStatus.FULLYBUILT,
+ distro_arch_series=arch_series_386,
+ )
# Failed build (i386)
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.FAILEDTOBUILD,
- distro_arch_series=arch_series_386)
+ recipe=ocirecipe,
+ status=BuildStatus.FAILEDTOBUILD,
+ distro_arch_series=arch_series_386,
+ )
# Building build (i386)
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.BUILDING,
- distro_arch_series=arch_series_386)
+ recipe=ocirecipe,
+ status=BuildStatus.BUILDING,
+ distro_arch_series=arch_series_386,
+ )
# Building build (hppa)
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.BUILDING,
- distro_arch_series=arch_series_hppa)
+ recipe=ocirecipe,
+ status=BuildStatus.BUILDING,
+ distro_arch_series=arch_series_hppa,
+ )
+ self.assertFalse(ocirecipe._hasPendingBuilds([arch_series_386]))
+ self.assertFalse(ocirecipe._hasPendingBuilds([arch_series_hppa]))
self.assertFalse(
- ocirecipe._hasPendingBuilds([arch_series_386]))
- self.assertFalse(
- ocirecipe._hasPendingBuilds([arch_series_hppa]))
- self.assertFalse(
- ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa]))
+ ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa])
+ )
# The only pending build, for i386.
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.NEEDSBUILD,
- distro_arch_series=arch_series_386)
+ recipe=ocirecipe,
+ status=BuildStatus.NEEDSBUILD,
+ distro_arch_series=arch_series_386,
+ )
- self.assertTrue(
- ocirecipe._hasPendingBuilds([arch_series_386]))
- self.assertFalse(
- ocirecipe._hasPendingBuilds([arch_series_hppa]))
+ self.assertTrue(ocirecipe._hasPendingBuilds([arch_series_386]))
+ self.assertFalse(ocirecipe._hasPendingBuilds([arch_series_hppa]))
self.assertFalse(
- ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa]))
+ ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa])
+ )
# Add a pending for hppa
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.NEEDSBUILD,
- distro_arch_series=arch_series_hppa)
+ recipe=ocirecipe,
+ status=BuildStatus.NEEDSBUILD,
+ distro_arch_series=arch_series_hppa,
+ )
self.assertTrue(
- ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa]))
+ ocirecipe._hasPendingBuilds([arch_series_386, arch_series_hppa])
+ )
def test_requestBuild(self):
ocirecipe = self.factory.makeOCIRecipe()
@@ -261,148 +268,214 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
self.assertRaises(
OCIRecipeBuildAlreadyPending,
ocirecipe.requestBuild,
- ocirecipe.owner, oci_arch)
+ ocirecipe.owner,
+ oci_arch,
+ )
def test_requestBuild_triggers_webhooks(self):
# Requesting a build triggers webhooks.
logger = self.useFixture(FakeLogger())
- with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
- OCI_RECIPE_ALLOW_CREATE: 'on'}):
+ with FeatureFixture(
+ {
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ }
+ ):
recipe = self.factory.makeOCIRecipe()
oci_arch = self.factory.makeOCIRecipeArch(recipe=recipe)
hook = self.factory.makeWebhook(
- target=recipe, event_types=["oci-recipe:build:0.1"])
+ target=recipe, event_types=["oci-recipe:build:0.1"]
+ )
build = recipe.requestBuild(recipe.owner, oci_arch)
expected_payload = {
"recipe_build": Equals(
- canonical_url(build, force_local_path=True)),
+ canonical_url(build, force_local_path=True)
+ ),
"action": Equals("created"),
"recipe": Equals(canonical_url(recipe, force_local_path=True)),
"build_request": Is(None),
"status": Equals("Needs building"),
- 'registry_upload_status': Equals('Unscheduled'),
- }
+ "registry_upload_status": Equals("Unscheduled"),
+ }
with person_logged_in(recipe.owner):
delivery = hook.deliveries.one()
self.assertThat(
- delivery, MatchesStructure(
+ delivery,
+ MatchesStructure(
event_type=Equals("oci-recipe:build:0.1"),
- payload=MatchesDict(expected_payload)))
+ payload=MatchesDict(expected_payload),
+ ),
+ )
with dbuser(config.IWebhookDeliveryJobSource.dbuser):
self.assertEqual(
- "<WebhookDeliveryJob for webhook %d on %r>" % (
- hook.id, hook.target),
- repr(delivery))
+ "<WebhookDeliveryJob for webhook %d on %r>"
+ % (hook.id, hook.target),
+ repr(delivery),
+ )
self.assertThat(
logger.output,
- LogsScheduledWebhooks([
- (hook, "oci-recipe:build:0.1",
- MatchesDict(expected_payload))]))
+ LogsScheduledWebhooks(
+ [
+ (
+ hook,
+ "oci-recipe:build:0.1",
+ MatchesDict(expected_payload),
+ )
+ ]
+ ),
+ )
def test_requestBuildsFromJob_creates_builds(self):
- ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe(
- require_virtualized=False))
+ ocirecipe = removeSecurityProxy(
+ self.factory.makeOCIRecipe(require_virtualized=False)
+ )
owner = ocirecipe.owner
distro = ocirecipe.oci_project.distribution
series = self.factory.makeDistroSeries(
- distribution=distro, status=SeriesStatus.CURRENT)
+ distribution=distro, status=SeriesStatus.CURRENT
+ )
arch_series_386 = self.getDistroArchSeries(series, "386", "386")
arch_series_hppa = self.getDistroArchSeries(series, "hppa", "hppa")
job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
- ocirecipe, owner)
+ ocirecipe, owner
+ )
with person_logged_in(job.requester):
builds = ocirecipe.requestBuildsFromJob(
- job.requester, build_request=job.build_request)
- self.assertThat(builds, MatchesSetwise(
- MatchesStructure(
- recipe=Equals(ocirecipe),
- processor=Equals(arch_series_386.processor)),
- MatchesStructure(
- recipe=Equals(ocirecipe),
- processor=Equals(arch_series_hppa.processor))
- ))
+ job.requester, build_request=job.build_request
+ )
+ self.assertThat(
+ builds,
+ MatchesSetwise(
+ MatchesStructure(
+ recipe=Equals(ocirecipe),
+ processor=Equals(arch_series_386.processor),
+ ),
+ MatchesStructure(
+ recipe=Equals(ocirecipe),
+ processor=Equals(arch_series_hppa.processor),
+ ),
+ ),
+ )
def test_requestBuildsFromJob_unauthorized_user(self):
ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe())
self.factory.makeDistroSeries(
distribution=ocirecipe.oci_project.distribution,
- status=SeriesStatus.CURRENT)
+ status=SeriesStatus.CURRENT,
+ )
another_user = self.factory.makePerson()
job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
- ocirecipe, another_user)
+ ocirecipe, another_user
+ )
with person_logged_in(job.requester):
self.assertRaises(
OCIRecipeNotOwner,
ocirecipe.requestBuildsFromJob,
- job.requester, build_request=job.build_request)
+ job.requester,
+ build_request=job.build_request,
+ )
def test_requestBuildsFromJob_with_pending_jobs(self):
ocirecipe = removeSecurityProxy(self.factory.makeOCIRecipe())
distro = ocirecipe.oci_project.distribution
series = self.factory.makeDistroSeries(
- distribution=distro, status=SeriesStatus.CURRENT)
+ distribution=distro, status=SeriesStatus.CURRENT
+ )
arch_series_386 = self.getDistroArchSeries(series, "386", "386")
self.factory.makeOCIRecipeBuild(
- recipe=ocirecipe, status=BuildStatus.NEEDSBUILD,
- distro_arch_series=arch_series_386)
+ recipe=ocirecipe,
+ status=BuildStatus.NEEDSBUILD,
+ distro_arch_series=arch_series_386,
+ )
job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
- ocirecipe, ocirecipe.owner)
+ ocirecipe, ocirecipe.owner
+ )
with person_logged_in(job.requester):
self.assertRaises(
OCIRecipeBuildAlreadyPending,
ocirecipe.requestBuildsFromJob,
- job.requester, build_request=job.build_request)
+ job.requester,
+ build_request=job.build_request,
+ )
def test_requestBuildsFromJob_triggers_webhooks(self):
# requestBuildsFromJob triggers webhooks, and the payload includes a
# link to the build request.
- self.useFixture(FeatureFixture({
- OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
- OCI_RECIPE_ALLOW_CREATE: "on",
- }))
- recipe = removeSecurityProxy(self.factory.makeOCIRecipe(
- require_virtualized=False))
+ self.useFixture(
+ FeatureFixture(
+ {
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ }
+ )
+ )
+ recipe = removeSecurityProxy(
+ self.factory.makeOCIRecipe(require_virtualized=False)
+ )
distro = recipe.oci_project.distribution
series = self.factory.makeDistroSeries(
- distribution=distro, status=SeriesStatus.CURRENT)
+ distribution=distro, status=SeriesStatus.CURRENT
+ )
self.getDistroArchSeries(series, "386", "386")
self.getDistroArchSeries(series, "hppa", "hppa")
job = getUtility(IOCIRecipeRequestBuildsJobSource).create(
- recipe, recipe.owner)
+ recipe, recipe.owner
+ )
logger = self.useFixture(FakeLogger())
hook = self.factory.makeWebhook(
- target=job.recipe, event_types=["oci-recipe:build:0.1"])
+ target=job.recipe, event_types=["oci-recipe:build:0.1"]
+ )
with person_logged_in(job.requester):
builds = recipe.requestBuildsFromJob(
- job.requester, build_request=job.build_request)
+ job.requester, build_request=job.build_request
+ )
self.assertEqual(2, len(builds))
payload_matchers = [
- MatchesDict({
- "recipe_build": Equals(canonical_url(
- build, force_local_path=True)),
- "action": Equals("created"),
- "recipe": Equals(canonical_url(
- job.recipe, force_local_path=True)),
- "build_request": Equals(canonical_url(
- job.build_request, force_local_path=True)),
- "status": Equals("Needs building"),
- "registry_upload_status": Equals("Unscheduled"),
- })
- for build in builds]
- self.assertThat(hook.deliveries, MatchesSetwise(*(
- MatchesStructure(
- event_type=Equals("oci-recipe:build:0.1"),
- payload=payload_matcher)
- for payload_matcher in payload_matchers)))
+ MatchesDict(
+ {
+ "recipe_build": Equals(
+ canonical_url(build, force_local_path=True)
+ ),
+ "action": Equals("created"),
+ "recipe": Equals(
+ canonical_url(job.recipe, force_local_path=True)
+ ),
+ "build_request": Equals(
+ canonical_url(
+ job.build_request, force_local_path=True
+ )
+ ),
+ "status": Equals("Needs building"),
+ "registry_upload_status": Equals("Unscheduled"),
+ }
+ )
+ for build in builds
+ ]
+ self.assertThat(
+ hook.deliveries,
+ MatchesSetwise(
+ *(
+ MatchesStructure(
+ event_type=Equals("oci-recipe:build:0.1"),
+ payload=payload_matcher,
+ )
+ for payload_matcher in payload_matchers
+ )
+ ),
+ )
self.assertThat(
logger.output,
- LogsScheduledWebhooks([
- (hook, "oci-recipe:build:0.1", payload_matcher)
- for payload_matcher in payload_matchers]))
+ LogsScheduledWebhooks(
+ [
+ (hook, "oci-recipe:build:0.1", payload_matcher)
+ for payload_matcher in payload_matchers
+ ]
+ ),
+ )
def test_destroySelf(self):
self.setConfig()
@@ -411,12 +484,15 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
build_request = oci_recipe.requestBuilds(oci_recipe.owner, ["386"])
build_ids = [
self.factory.makeOCIRecipeBuild(
- recipe=oci_recipe, build_request=build_request).id
- for _ in range(3)]
+ recipe=oci_recipe, build_request=build_request
+ ).id
+ for _ in range(3)
+ ]
# Create associated push rules:
push_rule_ids = [
self.factory.makeOCIPushRule(recipe=oci_recipe).id
- for i in range(3)]
+ for i in range(3)
+ ]
with person_logged_in(oci_recipe.owner):
oci_recipe.destroySelf()
@@ -426,12 +502,17 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
self.assertIsNone(getUtility(IOCIRecipeBuildSet).getByID(build_id))
for push_rule_id in push_rule_ids:
self.assertIsNone(
- getUtility(IOCIPushRuleSet).getByID(push_rule_id))
+ getUtility(IOCIPushRuleSet).getByID(push_rule_id)
+ )
def test_related_webhooks_deleted(self):
owner = self.factory.makePerson()
- with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
- OCI_RECIPE_ALLOW_CREATE: 'on'}):
+ with FeatureFixture(
+ {
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ }
+ ):
recipe = self.factory.makeOCIRecipe(registrant=owner, owner=owner)
webhook = self.factory.makeWebhook(target=recipe)
with person_logged_in(recipe.owner):
@@ -443,8 +524,10 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
def test_getBuilds(self):
# Test the various getBuilds methods.
oci_recipe = self.factory.makeOCIRecipe()
- builds = [self.factory.makeOCIRecipeBuild(recipe=oci_recipe)
- for x in range(3)]
+ builds = [
+ self.factory.makeOCIRecipeBuild(recipe=oci_recipe)
+ for x in range(3)
+ ]
# We want the latest builds first.
builds.reverse()
@@ -469,7 +552,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
instacancelled.updateStatus(BuildStatus.CANCELLED)
self.assertEqual([fullybuilt, instacancelled], list(oci_recipe.builds))
self.assertEqual(
- [fullybuilt, instacancelled], list(oci_recipe.completed_builds))
+ [fullybuilt, instacancelled], list(oci_recipe.completed_builds)
+ )
self.assertEqual([], list(oci_recipe.pending_builds))
def test_push_rules(self):
@@ -490,18 +574,30 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
url = self.factory.getUniqueURL()
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
with person_logged_in(recipe.registrant):
push_rule = recipe.newPushRule(
- recipe.registrant, url, image_name, credentials,
- credentials_owner=recipe.registrant)
- self.assertThat(push_rule, MatchesStructure(
- image_name=Equals(image_name),
- registry_credentials=MatchesOCIRegistryCredentials(
- MatchesStructure.byEquality(
- owner=recipe.registrant, url=url),
- Equals(credentials))))
+ recipe.registrant,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=recipe.registrant,
+ )
+ self.assertThat(
+ push_rule,
+ MatchesStructure(
+ image_name=Equals(image_name),
+ registry_credentials=MatchesOCIRegistryCredentials(
+ MatchesStructure.byEquality(
+ owner=recipe.registrant, url=url
+ ),
+ Equals(credentials),
+ ),
+ ),
+ )
self.assertEqual(push_rule, recipe.push_rules[0])
def test_newPushRule_default_owner(self):
@@ -512,31 +608,48 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
url = self.factory.getUniqueURL()
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
with person_logged_in(recipe.registrant):
push_rule = recipe.newPushRule(
- recipe.registrant, url, image_name, credentials)
- self.assertThat(push_rule, MatchesStructure(
- image_name=Equals(image_name),
- registry_credentials=MatchesOCIRegistryCredentials(
- MatchesStructure.byEquality(owner=recipe.owner, url=url),
- Equals(credentials))))
+ recipe.registrant, url, image_name, credentials
+ )
+ self.assertThat(
+ push_rule,
+ MatchesStructure(
+ image_name=Equals(image_name),
+ registry_credentials=MatchesOCIRegistryCredentials(
+ MatchesStructure.byEquality(
+ owner=recipe.owner, url=url
+ ),
+ Equals(credentials),
+ ),
+ ),
+ )
self.assertEqual(push_rule, recipe.push_rules[0])
def test_newPushRule_invalid_url(self):
self.setConfig()
recipe = self.factory.makeOCIRecipe()
- url = 'asdf://foo.com'
+ url = "asdf://foo.com"
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
with person_logged_in(recipe.owner):
self.assertRaises(
- ValidationError, recipe.newPushRule,
- recipe.owner, url, image_name, credentials,
- credentials_owner=recipe.owner)
+ ValidationError,
+ recipe.newPushRule,
+ recipe.owner,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=recipe.owner,
+ )
# Avoid trying to flush the incomplete object on cleanUp.
Store.of(recipe).rollback()
@@ -546,17 +659,27 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
url = self.factory.getUniqueURL()
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
with person_logged_in(recipe.owner):
recipe.newPushRule(
- recipe.owner, url, image_name, credentials,
- credentials_owner=recipe.owner)
+ recipe.owner,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=recipe.owner,
+ )
self.assertRaises(
OCIPushRuleAlreadyExists,
recipe.newPushRule,
- recipe.owner, url, image_name, credentials,
- credentials_owner=recipe.owner)
+ recipe.owner,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=recipe.owner,
+ )
def test_newPushRule_not_owner(self):
# If the registrant is not the owner or a member of the owner team,
@@ -566,25 +689,41 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
url = self.factory.getUniqueURL()
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
other_person = self.factory.makePerson()
other_team = self.factory.makeTeam(owner=other_person)
with person_logged_in(recipe.registrant):
expected_message = "%s cannot create credentials owned by %s." % (
- recipe.registrant.display_name, other_person.display_name)
+ recipe.registrant.display_name,
+ other_person.display_name,
+ )
with ExpectedException(
- OCIRegistryCredentialsNotOwner, expected_message):
+ OCIRegistryCredentialsNotOwner, expected_message
+ ):
recipe.newPushRule(
- recipe.registrant, url, image_name, credentials,
- credentials_owner=other_person)
+ recipe.registrant,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=other_person,
+ )
expected_message = "%s is not a member of %s." % (
- recipe.registrant.display_name, other_team.display_name)
+ recipe.registrant.display_name,
+ other_team.display_name,
+ )
with ExpectedException(
- OCIRegistryCredentialsNotOwner, expected_message):
+ OCIRegistryCredentialsNotOwner, expected_message
+ ):
recipe.newPushRule(
- recipe.registrant, url, image_name, credentials,
- credentials_owner=other_team)
+ recipe.registrant,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=other_team,
+ )
def test_newPushRule_distribution_credentials(self):
# If the OCIRecipe is in a Distribution with credentials set
@@ -598,41 +737,54 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
distribution.oci_registry_credentials = credentials
project.setOfficialRecipeStatus(recipe, True)
- url = 'asdf://foo.com'
+ url = "asdf://foo.com"
image_name = self.factory.getUniqueUnicode()
credentials = {
- "username": "test-username", "password": "test-password"}
+ "username": "test-username",
+ "password": "test-password",
+ }
with person_logged_in(recipe.owner):
with ExpectedException(UsingDistributionCredentials):
recipe.newPushRule(
- recipe.registrant, url, image_name, credentials,
- credentials_owner=recipe.registrant)
+ recipe.registrant,
+ url,
+ image_name,
+ credentials,
+ credentials_owner=recipe.registrant,
+ )
def test_set_official_directly_is_forbidden(self):
recipe = self.factory.makeOCIRecipe()
self.assertRaises(
- ForbiddenAttribute, setattr, recipe, 'official', True)
+ ForbiddenAttribute, setattr, recipe, "official", True
+ )
def test_set_recipe_as_official_for_oci_project(self):
distro = self.factory.makeDistribution()
owner = distro.owner
login_person(owner)
oci_project1 = self.factory.makeOCIProject(
- registrant=owner, pillar=distro)
+ registrant=owner, pillar=distro
+ )
oci_project2 = self.factory.makeOCIProject(
- registrant=owner, pillar=distro)
+ registrant=owner, pillar=distro
+ )
oci_proj1_recipes = [
self.factory.makeOCIRecipe(
- oci_project=oci_project1, registrant=owner, owner=owner)
- for _ in range(3)]
+ oci_project=oci_project1, registrant=owner, owner=owner
+ )
+ for _ in range(3)
+ ]
# Recipes for project 2
oci_proj2_recipes = [
self.factory.makeOCIRecipe(
- oci_project=oci_project2, registrant=owner, owner=owner)
- for _ in range(2)]
+ oci_project=oci_project2, registrant=owner, owner=owner
+ )
+ for _ in range(2)
+ ]
self.assertTrue(oci_project1.getOfficialRecipes().is_empty())
self.assertTrue(oci_project2.getOfficialRecipes().is_empty())
@@ -650,7 +802,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
self.assertTrue(oci_project2.getOfficialRecipes().is_empty())
self.assertEqual(
- oci_proj1_recipes[0], oci_project1.getOfficialRecipes()[0])
+ oci_proj1_recipes[0], oci_project1.getOfficialRecipes()[0]
+ )
self.assertTrue(oci_proj1_recipes[0].official)
for recipe in oci_proj1_recipes[1:] + oci_proj2_recipes:
self.assertFalse(recipe.official)
@@ -669,28 +822,36 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
owner = distro.owner
login_person(owner)
oci_project = self.factory.makeOCIProject(
- registrant=owner, pillar=distro)
+ registrant=owner, pillar=distro
+ )
another_oci_project = self.factory.makeOCIProject(
- registrant=owner, pillar=distro)
+ registrant=owner, pillar=distro
+ )
recipe = self.factory.makeOCIRecipe(
- oci_project=oci_project, registrant=owner)
+ oci_project=oci_project, registrant=owner
+ )
self.assertRaises(
OCIProjectRecipeInvalid,
- another_oci_project.setOfficialRecipeStatus, recipe, True)
+ another_oci_project.setOfficialRecipeStatus,
+ recipe,
+ True,
+ )
def test_permission_check_on_setOfficialRecipe(self):
distro = self.factory.makeDistribution()
owner = distro.owner
login_person(owner)
oci_project = self.factory.makeOCIProject(
- registrant=owner, pillar=distro)
+ registrant=owner, pillar=distro
+ )
another_user = self.factory.makePerson()
with person_logged_in(another_user):
self.assertRaises(
- Unauthorized, getattr, oci_project, 'setOfficialRecipeStatus')
+ Unauthorized, getattr, oci_project, "setOfficialRecipeStatus"
+ )
def test_oci_project_get_recipe_by_name_and_owner(self):
owner = self.factory.makePerson()
@@ -698,16 +859,22 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
oci_project = self.factory.makeOCIProject(registrant=owner)
recipe = self.factory.makeOCIRecipe(
- oci_project=oci_project, registrant=owner, owner=owner,
- name="foo-recipe")
+ oci_project=oci_project,
+ registrant=owner,
+ owner=owner,
+ name="foo-recipe",
+ )
self.assertEqual(
recipe,
- oci_project.getRecipeByNameAndOwner(recipe.name, owner.name))
+ oci_project.getRecipeByNameAndOwner(recipe.name, owner.name),
+ )
self.assertIsNone(
- oci_project.getRecipeByNameAndOwner(recipe.name, "someone"))
+ oci_project.getRecipeByNameAndOwner(recipe.name, "someone")
+ )
self.assertIsNone(
- oci_project.getRecipeByNameAndOwner("some-recipe", owner.name))
+ oci_project.getRecipeByNameAndOwner("some-recipe", owner.name)
+ )
def test_search_recipe_from_oci_project(self):
owner = self.factory.makePerson()
@@ -716,13 +883,17 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
another_oci_project = self.factory.makeOCIProject(registrant=owner)
recipe1 = self.factory.makeOCIRecipe(
- name="a something", oci_project=oci_project, registrant=owner)
+ name="a something", oci_project=oci_project, registrant=owner
+ )
recipe2 = self.factory.makeOCIRecipe(
- name="banana", oci_project=oci_project, registrant=owner)
+ name="banana", oci_project=oci_project, registrant=owner
+ )
# Recipe from another project.
self.factory.makeOCIRecipe(
- name="something too", oci_project=another_oci_project,
- registrant=owner)
+ name="something too",
+ oci_project=another_oci_project,
+ registrant=owner,
+ )
self.assertEqual([recipe1], list(oci_project.searchRecipes("somet")))
self.assertEqual([recipe2], list(oci_project.searchRecipes("bana")))
@@ -740,25 +911,38 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
distro = self.factory.makeDistribution(oci_project_admin=team)
oci_project = self.factory.makeOCIProject(
- registrant=team, pillar=distro)
+ registrant=team, pillar=distro
+ )
recipe1 = self.factory.makeOCIRecipe(
- name="same-name", oci_project=oci_project,
- registrant=owner1, owner=owner1)
+ name="same-name",
+ oci_project=oci_project,
+ registrant=owner1,
+ owner=owner1,
+ )
recipe2 = self.factory.makeOCIRecipe(
- name="same-name", oci_project=oci_project,
- registrant=owner2, owner=owner2)
+ name="same-name",
+ oci_project=oci_project,
+ registrant=owner2,
+ owner=owner2,
+ )
recipe3 = self.factory.makeOCIRecipe(
- name="a-first", oci_project=oci_project,
- registrant=owner1, owner=owner1)
+ name="a-first",
+ oci_project=oci_project,
+ registrant=owner1,
+ owner=owner1,
+ )
# This one should be filtered out.
self.factory.makeOCIRecipe(
- name="xxx", oci_project=oci_project,
- registrant=owner3, owner=owner3)
+ name="xxx",
+ oci_project=oci_project,
+ registrant=owner3,
+ owner=owner3,
+ )
# It should be sorted by owner's name first, then recipe name.
self.assertEqual(
- [recipe3, recipe1, recipe2],
- list(oci_project.searchRecipes("a")))
+ [recipe3, recipe1, recipe2], list(oci_project.searchRecipes("a"))
+ )
def test_build_args_dict(self):
args = {"MY_VERSION": "1.0.3", "ANOTHER_VERSION": "2.9.88"}
@@ -776,8 +960,10 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
]
for invalid_build_args in invalid_build_args_set:
self.assertRaises(
- AssertionError, self.factory.makeOCIRecipe,
- build_args=invalid_build_args)
+ AssertionError,
+ self.factory.makeOCIRecipe,
+ build_args=invalid_build_args,
+ )
def test_build_args_flatten_dict(self):
# Makes sure we only store one level of key=pair, flattening to
@@ -791,11 +977,14 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
# Force fetch it from database
store = IStore(recipe)
store.invalidate(recipe)
- self.assertEqual({
- "VAR1": "{'something': [1, 2, 3]}",
- "VAR2": "123",
- "VAR3": "A string",
- }, recipe.build_args)
+ self.assertEqual(
+ {
+ "VAR1": "{'something': [1, 2, 3]}",
+ "VAR2": "123",
+ "VAR3": "A string",
+ },
+ recipe.build_args,
+ )
def test_use_distribution_credentials_set(self):
self.setConfig()
@@ -834,38 +1023,59 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
login_admin()
private_team = self.factory.makeTeam(
visibility=PersonVisibility.PRIVATE,
- membership_policy=TeamMembershipPolicy.MODERATED)
+ membership_policy=TeamMembershipPolicy.MODERATED,
+ )
owner = self.factory.makePerson(member_of=[private_team])
pillar = self.factory.makeProduct(
- owner=private_team, registrant=owner,
+ owner=private_team,
+ registrant=owner,
information_type=InformationType.PROPRIETARY,
- branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY,
+ )
oci_project = self.factory.makeOCIProject(
- registrant=owner, pillar=pillar)
+ registrant=owner, pillar=pillar
+ )
[private_git_ref] = self.factory.makeGitRefs(
- target=pillar, owner=owner,
+ target=pillar,
+ owner=owner,
information_type=InformationType.PROPRIETARY,
- paths=['refs/heads/v1.0-20.04'])
+ paths=["refs/heads/v1.0-20.04"],
+ )
private_recipe = self.factory.makeOCIRecipe(
- owner=private_team, registrant=owner,
- oci_project=oci_project, git_ref=private_git_ref,
- information_type=InformationType.PROPRIETARY)
+ owner=private_team,
+ registrant=owner,
+ oci_project=oci_project,
+ git_ref=private_git_ref,
+ information_type=InformationType.PROPRIETARY,
+ )
public_recipe = self.factory.makeOCIRecipe()
# Should not be able to make the recipe PUBLIC if it's linked to
self.assertRaises(
- OCIRecipePrivacyMismatch, setattr,
- private_recipe, 'information_type', InformationType.PUBLIC)
+ OCIRecipePrivacyMismatch,
+ setattr,
+ private_recipe,
+ "information_type",
+ InformationType.PUBLIC,
+ )
# We should not be able to link public recipe to a private repo.
self.assertRaises(
- OCIRecipePrivacyMismatch, setattr,
- public_recipe, 'git_ref', private_git_ref)
+ OCIRecipePrivacyMismatch,
+ setattr,
+ public_recipe,
+ "git_ref",
+ private_git_ref,
+ )
# We should not be able to link public recipe to a private owner.
self.assertRaises(
- OCIRecipePrivacyMismatch, setattr,
- public_recipe, 'owner', private_team)
+ OCIRecipePrivacyMismatch,
+ setattr,
+ public_recipe,
+ "owner",
+ private_team,
+ )
class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
@@ -878,30 +1088,43 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
def test_change_oci_project_pillar_reconciles_access(self):
person = self.factory.makePerson()
initial_project = self.factory.makeProduct(
- name='initial-project',
- owner=person, registrant=person)
+ name="initial-project", owner=person, registrant=person
+ )
final_project = self.factory.makeProduct(
- name='final-project',
- owner=person, registrant=person)
+ name="final-project", owner=person, registrant=person
+ )
oci_project = self.factory.makeOCIProject(
- ociprojectname='the-oci-project', pillar=initial_project,
- registrant=person)
+ ociprojectname="the-oci-project",
+ pillar=initial_project,
+ registrant=person,
+ )
recipes = []
for i in range(10):
- recipes.append(self.factory.makeOCIRecipe(
- registrant=person, owner=person,
- oci_project=oci_project,
- information_type=InformationType.USERDATA))
+ recipes.append(
+ self.factory.makeOCIRecipe(
+ registrant=person,
+ owner=person,
+ oci_project=oci_project,
+ information_type=InformationType.USERDATA,
+ )
+ )
access_artifacts = getUtility(IAccessArtifactSource).find(recipes)
- initial_access_policy = getUtility(IAccessPolicySource).find(
- [(initial_project, InformationType.USERDATA)]).one()
+ initial_access_policy = (
+ getUtility(IAccessPolicySource)
+ .find([(initial_project, InformationType.USERDATA)])
+ .one()
+ )
apasource = getUtility(IAccessPolicyArtifactSource)
policy_artifacts = apasource.find(
- [(recipe_artifact, initial_access_policy)
- for recipe_artifact in access_artifacts])
+ [
+ (recipe_artifact, initial_access_policy)
+ for recipe_artifact in access_artifacts
+ ]
+ )
self.assertEqual(
- {i.policy.pillar for i in policy_artifacts}, {initial_project})
+ {i.policy.pillar for i in policy_artifacts}, {initial_project}
+ )
# Changing OCI project's pillar should move the policy artifacts of
# all OCI recipes associated to the new pillar.
@@ -909,13 +1132,20 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
with admin_logged_in():
oci_project.pillar = final_project
- final_access_policy = getUtility(IAccessPolicySource).find(
- [(final_project, InformationType.USERDATA)]).one()
+ final_access_policy = (
+ getUtility(IAccessPolicySource)
+ .find([(final_project, InformationType.USERDATA)])
+ .one()
+ )
policy_artifacts = apasource.find(
- [(recipe_artifact, final_access_policy)
- for recipe_artifact in access_artifacts])
+ [
+ (recipe_artifact, final_access_policy)
+ for recipe_artifact in access_artifacts
+ ]
+ )
self.assertEqual(
- {i.policy.pillar for i in policy_artifacts}, {final_project})
+ {i.policy.pillar for i in policy_artifacts}, {final_project}
+ )
def getGrants(self, ocirecipe, person=None):
conditions = [AccessArtifact.ocirecipe == ocirecipe]
@@ -924,13 +1154,16 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
return IStore(AccessArtifactGrant).find(
AccessArtifactGrant,
AccessArtifactGrant.abstract_artifact_id == AccessArtifact.id,
- *conditions)
+ *conditions,
+ )
def test_reconcile_set_public(self):
owner = self.factory.makePerson()
recipe = self.factory.makeOCIRecipe(
- registrant=owner, owner=owner,
- information_type=InformationType.USERDATA)
+ registrant=owner,
+ owner=owner,
+ information_type=InformationType.USERDATA,
+ )
another_user = self.factory.makePerson()
with admin_logged_in():
recipe.subscribe(another_user, recipe.owner)
@@ -941,7 +1174,9 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
person=Equals(another_user),
recipe=Equals(recipe),
subscribed_by=Equals(recipe.owner),
- date_created=IsInstance(datetime)))
+ date_created=IsInstance(datetime),
+ ),
+ )
recipe.information_type = InformationType.PUBLIC
self.assertEqual(0, self.getGrants(recipe, another_user).count())
@@ -951,7 +1186,9 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
person=Equals(another_user),
recipe=Equals(recipe),
subscribed_by=Equals(recipe.owner),
- date_created=IsInstance(datetime)))
+ date_created=IsInstance(datetime),
+ ),
+ )
def test_owner_is_subscribed_automatically(self):
recipe = self.factory.makeOCIRecipe()
@@ -960,19 +1197,25 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
self.assertTrue(recipe.visibleByUser(owner))
self.assertIn(owner, recipe.subscribers)
with person_logged_in(owner):
- self.assertThat(recipe.getSubscription(owner), MatchesStructure(
- person=Equals(owner),
- subscribed_by=Equals(registrant),
- date_created=IsInstance(datetime)))
+ self.assertThat(
+ recipe.getSubscription(owner),
+ MatchesStructure(
+ person=Equals(owner),
+ subscribed_by=Equals(registrant),
+ date_created=IsInstance(datetime),
+ ),
+ )
def test_owner_can_grant_access(self):
owner = self.factory.makePerson()
recipe = self.factory.makeOCIRecipe(
- registrant=owner, owner=owner,
- information_type=InformationType.USERDATA)
+ registrant=owner,
+ owner=owner,
+ information_type=InformationType.USERDATA,
+ )
other_person = self.factory.makePerson()
with person_logged_in(other_person):
- self.assertRaises(Unauthorized, getattr, recipe, 'subscribe')
+ self.assertRaises(Unauthorized, getattr, recipe, "subscribe")
with person_logged_in(owner):
recipe.subscribe(other_person, owner)
self.assertIn(other_person, recipe.subscribers)
@@ -981,18 +1224,23 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
owner = self.factory.makePerson()
person = self.factory.makePerson()
recipe = self.factory.makeOCIRecipe(
- registrant=owner, owner=owner,
- information_type=InformationType.USERDATA)
+ registrant=owner,
+ owner=owner,
+ information_type=InformationType.USERDATA,
+ )
with person_logged_in(owner):
self.assertFalse(recipe.visibleByUser(person))
def test_private_is_visible_by_team_member(self):
person = self.factory.makePerson()
team = self.factory.makeTeam(
- members=[person], membership_policy=TeamMembershipPolicy.MODERATED)
+ members=[person], membership_policy=TeamMembershipPolicy.MODERATED
+ )
recipe = self.factory.makeOCIRecipe(
- owner=team, registrant=person,
- information_type=InformationType.USERDATA)
+ owner=team,
+ registrant=person,
+ information_type=InformationType.USERDATA,
+ )
with person_logged_in(team):
self.assertTrue(recipe.visibleByUser(person))
@@ -1000,8 +1248,10 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
person = self.factory.makePerson()
owner = self.factory.makePerson()
recipe = self.factory.makeOCIRecipe(
- registrant=owner, owner=owner,
- information_type=InformationType.USERDATA)
+ registrant=owner,
+ owner=owner,
+ information_type=InformationType.USERDATA,
+ )
with person_logged_in(owner):
self.assertFalse(recipe.visibleByUser(person))
@@ -1012,7 +1262,9 @@ class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
person=Equals(person),
recipe=Equals(recipe),
subscribed_by=Equals(recipe.owner),
-