launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25232
[Merge] ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master with ~pappacena/launchpad:ocirecipe-args as a prerequisite.
Commit message:
UI to manage build ARGs for OCI recipes
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/389931
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index c2dceb2..6b5b338 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -19,22 +19,32 @@ __all__ = [
'OCIRecipeView',
]
+
from lazr.restful.interface import (
copy_field,
use_template,
)
from zope.component import getUtility
from zope.formlib.form import FormFields
+<<<<<<< lib/lp/oci/browser/ocirecipe.py
from zope.formlib.widget import (
DisplayWidget,
renderElement,
)
+=======
+from zope.formlib.textwidgets import TextAreaWidget
+from zope.formlib.widget import CustomWidgetFactory
+>>>>>>> lib/lp/oci/browser/ocirecipe.py
from zope.interface import Interface
from zope.schema import (
Bool,
Choice,
List,
+<<<<<<< lib/lp/oci/browser/ocirecipe.py
Password,
+=======
+ Text,
+>>>>>>> lib/lp/oci/browser/ocirecipe.py
TextLine,
ValidationError,
)
@@ -242,6 +252,11 @@ class OCIRecipeView(LaunchpadView):
else:
return "Built on request"
+ @property
+ def build_args(self):
+ return "\n".join(
+ "%s=%s" % (k, v) for k, v in self.context.build_args.items())
+
def builds_for_recipe(recipe):
"""A list of interesting builds.
@@ -676,13 +691,51 @@ class IOCIRecipeEditSchema(Interface):
"description",
"git_ref",
"build_file",
+ "build_args",
"build_daily",
"require_virtualized",
"allow_internet",
])
-class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
+class OCIRecipeFormMixin:
+ """Mixin with common processing for both edit and add views."""
+ custom_widget_build_args = CustomWidgetFactory(
+ TextAreaWidget, height=5, width=100)
+
+ def createBuildArgsField(self):
+ """Create a form field for OCIRecipe.build_args attribute."""
+ if IOCIRecipe.providedBy(self.context):
+ default = "\n".join(
+ "%s=%s" % (k, v) for k, v in self.context.build_args.items())
+ else:
+ default = ""
+ return FormFields(Text(
+ __name__='build_args',
+ title=u'Extra build 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')
+ 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))
+ self.setFieldError("build_args", str(msg))
+ return
+ k, v = line.split('=', 1)
+ build_args[k] = v
+ data['build_args'] = build_args
+
+
+class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
+ OCIRecipeFormMixin):
"""View for creating OCI recipes."""
page_title = label = "Create a new OCI recipe"
@@ -706,6 +759,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
def setUpFields(self):
"""See `LaunchpadFormView`."""
super(OCIRecipeAddView, self).setUpFields()
+ self.form_fields += self.createBuildArgsField()
self.form_fields += self.createEnabledProcessors(
getUtility(IProcessorSet).getAll(),
"The architectures that this OCI recipe builds for. Some "
@@ -745,6 +799,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
"There is already an OCI recipe owned by %s in %s with "
"this name." % (
owner.display_name, self.context.display_name))
+ self.validateBuildArgs(data)
@action("Create OCI recipe", name="create")
def create_action(self, action, data):
@@ -752,7 +807,8 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
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"], processors=data["processors"])
+ build_daily=data["build_daily"], build_args=data["build_args"],
+ processors=data["processors"])
self.next_url = canonical_url(recipe)
@@ -794,7 +850,8 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView):
field_names = ("require_virtualized", "allow_internet")
-class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
+class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
+ OCIRecipeFormMixin):
"""View for editing OCI recipes."""
@property
@@ -816,6 +873,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
def setUpFields(self):
"""See `LaunchpadFormView`."""
super(OCIRecipeEditView, self).setUpFields()
+ self.form_fields += self.createBuildArgsField()
self.form_fields += self.createEnabledProcessors(
self.context.available_processors,
"The architectures that this OCI recipe builds for. Some "
@@ -855,7 +913,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
# This processor is restricted and currently
# enabled. Leave it untouched.
data["processors"].append(processor)
-
+ self.validateBuildArgs(data)
class OCIRecipeDeleteView(BaseOCIRecipeEditView):
"""View for deleting OCI recipes."""
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 8b17168..d910065 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -215,6 +215,26 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
"Build schedule:\nBuilt on request\nEdit OCI recipe\n",
MatchesTagText(content, "build-schedule"))
+ def test_create_new_recipe_with_build_args(self):
+ oci_project = self.factory.makeOCIProject()
+ [git_ref] = self.factory.makeGitRefs()
+ browser = self.getViewBrowser(
+ 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("Git repository").value = (
+ git_ref.repository.identity)
+ browser.getControl("Git branch").value = git_ref.path
+ browser.getControl("Extra build 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 ARG:\nVAR1=10\nVAR2=20",
+ MatchesTagText(content, "build-args"))
+
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(
@@ -466,6 +486,47 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
login_person(self.person)
self.assertRecipeProcessors(recipe, ["386", "amd64"])
+ def test_edit_build_args(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"})
+ browser = self.getViewBrowser(
+ 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"
+ browser.getControl("Update OCI recipe").click()
+ login_person(self.person)
+ IStore(recipe).reload(recipe)
+ self.assertEqual(
+ {"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"})
+ browser = self.getViewBrowser(
+ 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"
+ browser.getControl("Update OCI recipe").click()
+
+ # Error message should be shown.
+ content = find_main_content(browser.contents)
+ self.assertIn(
+ "'messed up text' at line 2 is not a valid KEY=value pair.",
+ extract_text(content))
+
+ # Assert that recipe still have the original build_args.
+ login_person(self.person)
+ IStore(recipe).reload(recipe)
+ self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args)
+
def test_edit_with_invisible_processor(self):
# It's possible for existing recipes to have an enabled processor
# that's no longer usable with the current distroseries, which will
@@ -705,6 +766,37 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
""" % (oci_project_name, oci_project_display),
self.getMainText(build.recipe))
+ def test_index_with_build_args(self):
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
+ oci_project_name = oci_project.name
+ oci_project_display = oci_project.display_name
+ [ref] = self.factory.makeGitRefs(
+ owner=self.person, target=self.person, name="recipe-repository",
+ paths=["refs/heads/master"])
+ recipe = self.makeOCIRecipe(
+ oci_project=oci_project, git_ref=ref, build_file="Dockerfile",
+ build_args={"VAR1": "123", "VAR2": "XXX"})
+ build = self.makeBuild(
+ recipe=recipe, status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30))
+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ %s OCI project
+ recipe-name
+ .*
+ OCI recipe information
+ Owner: Test Person
+ OCI project: %s
+ Source: ~test-person/\\+git/recipe-repository:master
+ Build file path: Dockerfile
+ Build schedule: Built on request
+ Build ARG: VAR1=123 VAR2=XXX
+ Latest builds
+ Status When complete Architecture
+ Successfully built 30 minutes ago 386
+ """ % (oci_project_name, oci_project_display),
+ self.getMainText(build.recipe))
+
def test_index_success_with_buildlog(self):
# The build log is shown if it is there.
build = self.makeBuild(
diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
index 21a6939..2de9c29 100644
--- a/lib/lp/oci/templates/ocirecipe-index.pt
+++ b/lib/lp/oci/templates/ocirecipe-index.pt
@@ -64,6 +64,14 @@
<a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
</dd>
</dl>
+ <dl id="build-args"
+ tal:define="build_args view/build_args"
+ tal:condition="build_args">
+ <dt>Build ARG:</dt>
+ <dd>
+ <pre><span tal:replace="build_args"/></pre>
+ </dd>
+ </dl>
</div>
<h2>Latest builds</h2>
diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
index e5f2686..13c93c3 100644
--- a/lib/lp/oci/templates/ocirecipe-new.pt
+++ b/lib/lp/oci/templates/ocirecipe-new.pt
@@ -32,6 +32,9 @@
<tal:widget define="widget nocall:view/widgets/build_daily">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
+ <tal:widget define="widget nocall:view/widgets/build_args">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
<tal:widget define="widget nocall:view/widgets/processors">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
Follow ups