Merge ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 04c9ed5f8bab13f5c0485f6d41fa38607248c295
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:ui-ocirecipe-args
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:ocirecipe-args
Diff against target: 302 lines (+166/-6)
4 files modified
lib/lp/oci/browser/ocirecipe.py (+59/-6)
lib/lp/oci/browser/tests/test_ocirecipe.py (+92/-0)
lib/lp/oci/templates/ocirecipe-index.pt (+12/-0)
lib/lp/oci/templates/ocirecipe-new.pt (+3/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+389931@code.launchpad.net

This proposal supersedes a proposal from 2020-08-27.

Commit message

UI to manage build ARGs for OCI recipes

To post a comment you must log in.
6919ec2... by Thiago F. Pappacena

Merge branch 'ocirecipe-args' into ui-ocirecipe-args

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

I forgot to add the screenshots for this. I'll do it in some minutes.

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

Merge branch 'ocirecipe-args' into ui-ocirecipe-args

Revision history for this message
Tom Wardill (twom) :
0c1d56d... by Thiago F. Pappacena

Small adjustments on build ARG presentation

Revision history for this message
Thiago F. Pappacena (pappacena) :
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) :
2fb9015... by Thiago F. Pappacena

Fixing tag placement

04c9ed5... by Thiago F. Pappacena

Merge branch 'ocirecipe-args' into ui-ocirecipe-args

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 5e6a50f..cf11bfb 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -23,9 +23,12 @@ from lazr.restful.interface import (
23 copy_field,23 copy_field,
24 use_template,24 use_template,
25 )25 )
26import six
26from zope.component import getUtility27from zope.component import getUtility
27from zope.formlib.form import FormFields28from zope.formlib.form import FormFields
29from zope.formlib.textwidgets import TextAreaWidget
28from zope.formlib.widget import (30from zope.formlib.widget import (
31 CustomWidgetFactory,
29 DisplayWidget,32 DisplayWidget,
30 renderElement,33 renderElement,
31 )34 )
@@ -35,6 +38,7 @@ from zope.schema import (
35 Choice,38 Choice,
36 List,39 List,
37 Password,40 Password,
41 Text,
38 TextLine,42 TextLine,
39 ValidationError,43 ValidationError,
40 )44 )
@@ -242,6 +246,12 @@ class OCIRecipeView(LaunchpadView):
242 else:246 else:
243 return "Built on request"247 return "Built on request"
244248
249 @property
250 def build_args(self):
251 return "\n".join(
252 "%s=%s" % (k, v)
253 for k, v in sorted(self.context.build_args.items()))
254
245255
246def builds_for_recipe(recipe):256def builds_for_recipe(recipe):
247 """A list of interesting builds.257 """A list of interesting builds.
@@ -676,6 +686,7 @@ class IOCIRecipeEditSchema(Interface):
676 "description",686 "description",
677 "git_ref",687 "git_ref",
678 "build_file",688 "build_file",
689 "build_args",
679 "build_path",690 "build_path",
680 "build_daily",691 "build_daily",
681 "require_virtualized",692 "require_virtualized",
@@ -683,7 +694,45 @@ class IOCIRecipeEditSchema(Interface):
683 ])694 ])
684695
685696
686class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):697class OCIRecipeFormMixin:
698 """Mixin with common processing for both edit and add views."""
699 custom_widget_build_args = CustomWidgetFactory(
700 TextAreaWidget, height=5, width=100)
701
702 def createBuildArgsField(self):
703 """Create a form field for OCIRecipe.build_args attribute."""
704 if IOCIRecipe.providedBy(self.context):
705 default = "\n".join(
706 "%s=%s" % (k, v)
707 for k, v in sorted(self.context.build_args.items()))
708 else:
709 default = ""
710 return FormFields(Text(
711 __name__='build_args',
712 title=u'Build-time ARG variables',
713 description=("One per line. Each ARG should be in the format "
714 "of ARG_KEY=arg_value."),
715 default=default,
716 required=False, readonly=False))
717
718 def validateBuildArgs(self, data):
719 field_value = data.get('build_args')
720 if not field_value:
721 return
722 build_args = {}
723 for i, line in enumerate(field_value.split("\n")):
724 if '=' not in line:
725 msg = ("'%s' at line %s is not a valid KEY=value pair." %
726 (line, i + 1))
727 self.setFieldError("build_args", six.text_type(msg))
728 return
729 k, v = line.split('=', 1)
730 build_args[k] = v
731 data['build_args'] = build_args
732
733
734class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
735 OCIRecipeFormMixin):
687 """View for creating OCI recipes."""736 """View for creating OCI recipes."""
688737
689 page_title = label = "Create a new OCI recipe"738 page_title = label = "Create a new OCI recipe"
@@ -708,6 +757,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
708 def setUpFields(self):757 def setUpFields(self):
709 """See `LaunchpadFormView`."""758 """See `LaunchpadFormView`."""
710 super(OCIRecipeAddView, self).setUpFields()759 super(OCIRecipeAddView, self).setUpFields()
760 self.form_fields += self.createBuildArgsField()
711 self.form_fields += self.createEnabledProcessors(761 self.form_fields += self.createEnabledProcessors(
712 getUtility(IProcessorSet).getAll(),762 getUtility(IProcessorSet).getAll(),
713 "The architectures that this OCI recipe builds for. Some "763 "The architectures that this OCI recipe builds for. Some "
@@ -748,15 +798,16 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
748 "There is already an OCI recipe owned by %s in %s with "798 "There is already an OCI recipe owned by %s in %s with "
749 "this name." % (799 "this name." % (
750 owner.display_name, self.context.display_name))800 owner.display_name, self.context.display_name))
801 self.validateBuildArgs(data)
751802
752 @action("Create OCI recipe", name="create")803 @action("Create OCI recipe", name="create")
753 def create_action(self, action, data):804 def create_action(self, action, data):
754 recipe = getUtility(IOCIRecipeSet).new(805 recipe = getUtility(IOCIRecipeSet).new(
755 name=data["name"], registrant=self.user, owner=data["owner"],806 name=data["name"], registrant=self.user, owner=data["owner"],
756 oci_project=self.context, git_ref=data["git_ref"],807 oci_project=self.context, git_ref=data["git_ref"],
757 build_file=data["build_file"], build_path=data["build_path"],808 build_file=data["build_file"], description=data["description"],
758 description=data["description"],809 build_daily=data["build_daily"], build_args=data["build_args"],
759 build_daily=data["build_daily"], processors=data["processors"])810 build_path=data["build_path"], processors=data["processors"])
760 self.next_url = canonical_url(recipe)811 self.next_url = canonical_url(recipe)
761812
762813
@@ -798,7 +849,8 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView):
798 field_names = ("require_virtualized", "allow_internet")849 field_names = ("require_virtualized", "allow_internet")
799850
800851
801class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):852class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
853 OCIRecipeFormMixin):
802 """View for editing OCI recipes."""854 """View for editing OCI recipes."""
803855
804 @property856 @property
@@ -821,6 +873,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
821 def setUpFields(self):873 def setUpFields(self):
822 """See `LaunchpadFormView`."""874 """See `LaunchpadFormView`."""
823 super(OCIRecipeEditView, self).setUpFields()875 super(OCIRecipeEditView, self).setUpFields()
876 self.form_fields += self.createBuildArgsField()
824 self.form_fields += self.createEnabledProcessors(877 self.form_fields += self.createEnabledProcessors(
825 self.context.available_processors,878 self.context.available_processors,
826 "The architectures that this OCI recipe builds for. Some "879 "The architectures that this OCI recipe builds for. Some "
@@ -860,7 +913,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
860 # This processor is restricted and currently913 # This processor is restricted and currently
861 # enabled. Leave it untouched.914 # enabled. Leave it untouched.
862 data["processors"].append(processor)915 data["processors"].append(processor)
863916 self.validateBuildArgs(data)
864917
865class OCIRecipeDeleteView(BaseOCIRecipeEditView):918class OCIRecipeDeleteView(BaseOCIRecipeEditView):
866 """View for deleting OCI recipes."""919 """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 c854d14..9a3f0ea 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):
215 "Build schedule:\nBuilt on request\nEdit OCI recipe\n",215 "Build schedule:\nBuilt on request\nEdit OCI recipe\n",
216 MatchesTagText(content, "build-schedule"))216 MatchesTagText(content, "build-schedule"))
217217
218 def test_create_new_recipe_with_build_args(self):
219 oci_project = self.factory.makeOCIProject()
220 [git_ref] = self.factory.makeGitRefs()
221 browser = self.getViewBrowser(
222 oci_project, view_name="+new-recipe", user=self.person)
223 browser.getControl(name="field.name").value = "recipe-name"
224 browser.getControl("Description").value = "Recipe description"
225 browser.getControl("Git repository").value = (
226 git_ref.repository.identity)
227 browser.getControl("Git branch").value = git_ref.path
228 browser.getControl("Build-time ARG variables").value = (
229 "VAR1=10\nVAR2=20")
230 browser.getControl("Create OCI recipe").click()
231
232 content = find_main_content(browser.contents)
233 self.assertEqual("recipe-name", extract_text(content.h1))
234 self.assertThat(
235 "Build-time\nARG variables:\nVAR1=10\nVAR2=20",
236 MatchesTagText(content, "build-args"))
237
218 def test_create_new_recipe_users_teams_as_owner_options(self):238 def test_create_new_recipe_users_teams_as_owner_options(self):
219 # Teams that the user is in are options for the OCI recipe owner.239 # Teams that the user is in are options for the OCI recipe owner.
220 self.factory.makeTeam(240 self.factory.makeTeam(
@@ -466,6 +486,47 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
466 login_person(self.person)486 login_person(self.person)
467 self.assertRecipeProcessors(recipe, ["386", "amd64"])487 self.assertRecipeProcessors(recipe, ["386", "amd64"])
468488
489 def test_edit_build_args(self):
490 self.setUpDistroSeries()
491 oci_project = self.factory.makeOCIProject(pillar=self.distribution)
492 recipe = self.factory.makeOCIRecipe(
493 registrant=self.person, owner=self.person,
494 oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"})
495 browser = self.getViewBrowser(
496 recipe, view_name="+edit", user=recipe.owner)
497 args = browser.getControl(name="field.build_args")
498 self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value)
499 args.value = "VAR=aa\nANOTHER_VAR=bbb"
500 browser.getControl("Update OCI recipe").click()
501 login_person(self.person)
502 IStore(recipe).reload(recipe)
503 self.assertEqual(
504 {"VAR": "aa", "ANOTHER_VAR": "bbb"}, recipe.build_args)
505
506 def test_edit_build_args_invalid_content(self):
507 self.setUpDistroSeries()
508 oci_project = self.factory.makeOCIProject(pillar=self.distribution)
509 recipe = self.factory.makeOCIRecipe(
510 registrant=self.person, owner=self.person,
511 oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"})
512 browser = self.getViewBrowser(
513 recipe, view_name="+edit", user=recipe.owner)
514 args = browser.getControl(name="field.build_args")
515 self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value)
516 args.value = "VAR=aa\nmessed up text"
517 browser.getControl("Update OCI recipe").click()
518
519 # Error message should be shown.
520 content = find_main_content(browser.contents)
521 self.assertIn(
522 "'messed up text' at line 2 is not a valid KEY=value pair.",
523 extract_text(content))
524
525 # Assert that recipe still have the original build_args.
526 login_person(self.person)
527 IStore(recipe).reload(recipe)
528 self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args)
529
469 def test_edit_with_invisible_processor(self):530 def test_edit_with_invisible_processor(self):
470 # It's possible for existing recipes to have an enabled processor531 # It's possible for existing recipes to have an enabled processor
471 # that's no longer usable with the current distroseries, which will532 # that's no longer usable with the current distroseries, which will
@@ -705,6 +766,37 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
705 """ % (oci_project_name, oci_project_display),766 """ % (oci_project_name, oci_project_display),
706 self.getMainText(build.recipe))767 self.getMainText(build.recipe))
707768
769 def test_index_with_build_args(self):
770 oci_project = self.factory.makeOCIProject(
771 pillar=self.distroseries.distribution)
772 oci_project_name = oci_project.name
773 oci_project_display = oci_project.display_name
774 [ref] = self.factory.makeGitRefs(
775 owner=self.person, target=self.person, name="recipe-repository",
776 paths=["refs/heads/master"])
777 recipe = self.makeOCIRecipe(
778 oci_project=oci_project, git_ref=ref, build_file="Dockerfile",
779 build_args={"VAR1": "123", "VAR2": "XXX"})
780 build = self.makeBuild(
781 recipe=recipe, status=BuildStatus.FULLYBUILT,
782 duration=timedelta(minutes=30))
783 self.assertTextMatchesExpressionIgnoreWhitespace("""\
784 %s OCI project
785 recipe-name
786 .*
787 OCI recipe information
788 Owner: Test Person
789 OCI project: %s
790 Source: ~test-person/\\+git/recipe-repository:master
791 Build file path: Dockerfile
792 Build schedule: Built on request
793 Build-time\nARG variables: VAR1=123 VAR2=XXX
794 Latest builds
795 Status When complete Architecture
796 Successfully built 30 minutes ago 386
797 """ % (oci_project_name, oci_project_display),
798 self.getMainText(build.recipe))
799
708 def test_index_success_with_buildlog(self):800 def test_index_success_with_buildlog(self):
709 # The build log is shown if it is there.801 # The build log is shown if it is there.
710 build = self.makeBuild(802 build = self.makeBuild(
diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
index c2ef69e..5c3ca56 100644
--- a/lib/lp/oci/templates/ocirecipe-index.pt
+++ b/lib/lp/oci/templates/ocirecipe-index.pt
@@ -69,6 +69,18 @@
69 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>69 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
70 </dd>70 </dd>
71 </dl>71 </dl>
72 <dl id="build-args"
73 tal:define="build_args view/build_args"
74 tal:condition="build_args">
75 <dt>
76 Build-time
77 <a href="https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg"
78 target="_blank">ARG variables</a>:
79 </dt>
80 <dd>
81 <pre tal:content="build_args" />
82 </dd>
83 </dl>
72 </div>84 </div>
7385
74 <h2>Latest builds</h2>86 <h2>Latest builds</h2>
diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
index 0bc156a..1cae71e 100644
--- a/lib/lp/oci/templates/ocirecipe-new.pt
+++ b/lib/lp/oci/templates/ocirecipe-new.pt
@@ -35,6 +35,9 @@
35 <tal:widget define="widget nocall:view/widgets/build_daily">35 <tal:widget define="widget nocall:view/widgets/build_daily">
36 <metal:block use-macro="context/@@launchpad_form/widget_row" />36 <metal:block use-macro="context/@@launchpad_form/widget_row" />
37 </tal:widget>37 </tal:widget>
38 <tal:widget define="widget nocall:view/widgets/build_args">
39 <metal:block use-macro="context/@@launchpad_form/widget_row" />
40 </tal:widget>
38 <tal:widget define="widget nocall:view/widgets/processors">41 <tal:widget define="widget nocall:view/widgets/processors">
39 <metal:block use-macro="context/@@launchpad_form/widget_row" />42 <metal:block use-macro="context/@@launchpad_form/widget_row" />
40 </tal:widget>43 </tal:widget>