Merge ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master
- Git
- lp:~pappacena/launchpad
- ui-ocirecipe-args
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Superseded |
---|---|
Proposed branch: | ~pappacena/launchpad:ui-ocirecipe-args |
Merge into: | launchpad:master |
Diff against target: |
619 lines (+251/-14) (has conflicts) 12 files modified
lib/lp/oci/browser/ocirecipe.py (+62/-4) lib/lp/oci/browser/tests/test_ocirecipe.py (+92/-0) lib/lp/oci/interfaces/ocirecipe.py (+8/-1) lib/lp/oci/model/ocirecipe.py (+16/-3) lib/lp/oci/model/ocirecipebuildbehaviour.py (+2/-1) lib/lp/oci/templates/ocirecipe-index.pt (+8/-0) lib/lp/oci/templates/ocirecipe-new.pt (+3/-0) lib/lp/oci/tests/test_ocirecipe.py (+43/-1) lib/lp/oci/tests/test_ocirecipebuildbehaviour.py (+3/-0) lib/lp/registry/interfaces/ociproject.py (+9/-1) lib/lp/registry/model/ociproject.py (+2/-1) lib/lp/testing/factory.py (+3/-2) Conflict in lib/lp/oci/browser/ocirecipe.py |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+389930@code.launchpad.net |
This proposal has been superseded by a proposal from 2020-08-27.
Commit message
UI to manage build ARGs for OCI recipes
Description of the change
To post a comment you must log in.
Unmerged commits
- 6619044... by Thiago F. Pappacena
-
Adding tests
- 0f6d016... by Thiago F. Pappacena
-
Merge branch 'ocirecipe-args' into ui-ocirecipe-args
- 7e10963... by Thiago F. Pappacena
-
Changing OCIRecipe.
build_args datatype to jsonb - 9554b98... by Thiago F. Pappacena
-
Adding build args field to recipe creation and index
- 3501818... by Thiago F. Pappacena
-
Adding form field to edit OCIRecipe.
build_args - 2bf5e96... by Thiago F. Pappacena
-
Merge branch 'ocirecipe-args' into ui-ocirecipe-args
- b8398e4... by Thiago F. Pappacena
-
Changing OCIRecipe.
build_args datatype to jsonb - 0392538... by Thiago F. Pappacena
-
Changing OCIRecipe.
build_args datatype to jsonb - 16e88a9... by Thiago F. Pappacena
-
Add OCIRecipe.
build_args as parameter to OCIRecipeBuildB ehaviour - bf2c7b1... by Thiago F. Pappacena
-
Adding Docker ARG option to OCI Recipe
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
2 | index c2dceb2..6b5b338 100644 |
3 | --- a/lib/lp/oci/browser/ocirecipe.py |
4 | +++ b/lib/lp/oci/browser/ocirecipe.py |
5 | @@ -19,22 +19,32 @@ __all__ = [ |
6 | 'OCIRecipeView', |
7 | ] |
8 | |
9 | + |
10 | from lazr.restful.interface import ( |
11 | copy_field, |
12 | use_template, |
13 | ) |
14 | from zope.component import getUtility |
15 | from zope.formlib.form import FormFields |
16 | +<<<<<<< lib/lp/oci/browser/ocirecipe.py |
17 | from zope.formlib.widget import ( |
18 | DisplayWidget, |
19 | renderElement, |
20 | ) |
21 | +======= |
22 | +from zope.formlib.textwidgets import TextAreaWidget |
23 | +from zope.formlib.widget import CustomWidgetFactory |
24 | +>>>>>>> lib/lp/oci/browser/ocirecipe.py |
25 | from zope.interface import Interface |
26 | from zope.schema import ( |
27 | Bool, |
28 | Choice, |
29 | List, |
30 | +<<<<<<< lib/lp/oci/browser/ocirecipe.py |
31 | Password, |
32 | +======= |
33 | + Text, |
34 | +>>>>>>> lib/lp/oci/browser/ocirecipe.py |
35 | TextLine, |
36 | ValidationError, |
37 | ) |
38 | @@ -242,6 +252,11 @@ class OCIRecipeView(LaunchpadView): |
39 | else: |
40 | return "Built on request" |
41 | |
42 | + @property |
43 | + def build_args(self): |
44 | + return "\n".join( |
45 | + "%s=%s" % (k, v) for k, v in self.context.build_args.items()) |
46 | + |
47 | |
48 | def builds_for_recipe(recipe): |
49 | """A list of interesting builds. |
50 | @@ -676,13 +691,51 @@ class IOCIRecipeEditSchema(Interface): |
51 | "description", |
52 | "git_ref", |
53 | "build_file", |
54 | + "build_args", |
55 | "build_daily", |
56 | "require_virtualized", |
57 | "allow_internet", |
58 | ]) |
59 | |
60 | |
61 | -class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): |
62 | +class OCIRecipeFormMixin: |
63 | + """Mixin with common processing for both edit and add views.""" |
64 | + custom_widget_build_args = CustomWidgetFactory( |
65 | + TextAreaWidget, height=5, width=100) |
66 | + |
67 | + def createBuildArgsField(self): |
68 | + """Create a form field for OCIRecipe.build_args attribute.""" |
69 | + if IOCIRecipe.providedBy(self.context): |
70 | + default = "\n".join( |
71 | + "%s=%s" % (k, v) for k, v in self.context.build_args.items()) |
72 | + else: |
73 | + default = "" |
74 | + return FormFields(Text( |
75 | + __name__='build_args', |
76 | + title=u'Extra build ARG variables', |
77 | + description=("One per line. Each ARG should be in the format " |
78 | + "of ARG_KEY=arg_value."), |
79 | + default=default, |
80 | + required=False, readonly=False)) |
81 | + |
82 | + def validateBuildArgs(self, data): |
83 | + field_value = data.get('build_args') |
84 | + if not field_value: |
85 | + return |
86 | + build_args = {} |
87 | + for i, line in enumerate(field_value.split("\n")): |
88 | + if '=' not in line: |
89 | + msg = ("'%s' at line %s is not a valid KEY=value pair." % |
90 | + (line, i + 1)) |
91 | + self.setFieldError("build_args", str(msg)) |
92 | + return |
93 | + k, v = line.split('=', 1) |
94 | + build_args[k] = v |
95 | + data['build_args'] = build_args |
96 | + |
97 | + |
98 | +class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
99 | + OCIRecipeFormMixin): |
100 | """View for creating OCI recipes.""" |
101 | |
102 | page_title = label = "Create a new OCI recipe" |
103 | @@ -706,6 +759,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): |
104 | def setUpFields(self): |
105 | """See `LaunchpadFormView`.""" |
106 | super(OCIRecipeAddView, self).setUpFields() |
107 | + self.form_fields += self.createBuildArgsField() |
108 | self.form_fields += self.createEnabledProcessors( |
109 | getUtility(IProcessorSet).getAll(), |
110 | "The architectures that this OCI recipe builds for. Some " |
111 | @@ -745,6 +799,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): |
112 | "There is already an OCI recipe owned by %s in %s with " |
113 | "this name." % ( |
114 | owner.display_name, self.context.display_name)) |
115 | + self.validateBuildArgs(data) |
116 | |
117 | @action("Create OCI recipe", name="create") |
118 | def create_action(self, action, data): |
119 | @@ -752,7 +807,8 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): |
120 | name=data["name"], registrant=self.user, owner=data["owner"], |
121 | oci_project=self.context, git_ref=data["git_ref"], |
122 | build_file=data["build_file"], description=data["description"], |
123 | - build_daily=data["build_daily"], processors=data["processors"]) |
124 | + build_daily=data["build_daily"], build_args=data["build_args"], |
125 | + processors=data["processors"]) |
126 | self.next_url = canonical_url(recipe) |
127 | |
128 | |
129 | @@ -794,7 +850,8 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView): |
130 | field_names = ("require_virtualized", "allow_internet") |
131 | |
132 | |
133 | -class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): |
134 | +class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, |
135 | + OCIRecipeFormMixin): |
136 | """View for editing OCI recipes.""" |
137 | |
138 | @property |
139 | @@ -816,6 +873,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): |
140 | def setUpFields(self): |
141 | """See `LaunchpadFormView`.""" |
142 | super(OCIRecipeEditView, self).setUpFields() |
143 | + self.form_fields += self.createBuildArgsField() |
144 | self.form_fields += self.createEnabledProcessors( |
145 | self.context.available_processors, |
146 | "The architectures that this OCI recipe builds for. Some " |
147 | @@ -855,7 +913,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): |
148 | # This processor is restricted and currently |
149 | # enabled. Leave it untouched. |
150 | data["processors"].append(processor) |
151 | - |
152 | + self.validateBuildArgs(data) |
153 | |
154 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): |
155 | """View for deleting OCI recipes.""" |
156 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
157 | index 8b17168..d910065 100644 |
158 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
159 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
160 | @@ -215,6 +215,26 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): |
161 | "Build schedule:\nBuilt on request\nEdit OCI recipe\n", |
162 | MatchesTagText(content, "build-schedule")) |
163 | |
164 | + def test_create_new_recipe_with_build_args(self): |
165 | + oci_project = self.factory.makeOCIProject() |
166 | + [git_ref] = self.factory.makeGitRefs() |
167 | + browser = self.getViewBrowser( |
168 | + oci_project, view_name="+new-recipe", user=self.person) |
169 | + browser.getControl(name="field.name").value = "recipe-name" |
170 | + browser.getControl("Description").value = "Recipe description" |
171 | + browser.getControl("Git repository").value = ( |
172 | + git_ref.repository.identity) |
173 | + browser.getControl("Git branch").value = git_ref.path |
174 | + browser.getControl("Extra build ARG variables").value = ( |
175 | + "VAR1=10\nVAR2=20") |
176 | + browser.getControl("Create OCI recipe").click() |
177 | + |
178 | + content = find_main_content(browser.contents) |
179 | + self.assertEqual("recipe-name", extract_text(content.h1)) |
180 | + self.assertThat( |
181 | + "Build ARG:\nVAR1=10\nVAR2=20", |
182 | + MatchesTagText(content, "build-args")) |
183 | + |
184 | def test_create_new_recipe_users_teams_as_owner_options(self): |
185 | # Teams that the user is in are options for the OCI recipe owner. |
186 | self.factory.makeTeam( |
187 | @@ -466,6 +486,47 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView): |
188 | login_person(self.person) |
189 | self.assertRecipeProcessors(recipe, ["386", "amd64"]) |
190 | |
191 | + def test_edit_build_args(self): |
192 | + self.setUpDistroSeries() |
193 | + oci_project = self.factory.makeOCIProject(pillar=self.distribution) |
194 | + recipe = self.factory.makeOCIRecipe( |
195 | + registrant=self.person, owner=self.person, |
196 | + oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"}) |
197 | + browser = self.getViewBrowser( |
198 | + recipe, view_name="+edit", user=recipe.owner) |
199 | + args = browser.getControl(name="field.build_args") |
200 | + self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value) |
201 | + args.value = "VAR=aa\nANOTHER_VAR=bbb" |
202 | + browser.getControl("Update OCI recipe").click() |
203 | + login_person(self.person) |
204 | + IStore(recipe).reload(recipe) |
205 | + self.assertEqual( |
206 | + {"VAR": "aa", "ANOTHER_VAR": "bbb"}, recipe.build_args) |
207 | + |
208 | + def test_edit_build_args_invalid_content(self): |
209 | + self.setUpDistroSeries() |
210 | + oci_project = self.factory.makeOCIProject(pillar=self.distribution) |
211 | + recipe = self.factory.makeOCIRecipe( |
212 | + registrant=self.person, owner=self.person, |
213 | + oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"}) |
214 | + browser = self.getViewBrowser( |
215 | + recipe, view_name="+edit", user=recipe.owner) |
216 | + args = browser.getControl(name="field.build_args") |
217 | + self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value) |
218 | + args.value = "VAR=aa\nmessed up text" |
219 | + browser.getControl("Update OCI recipe").click() |
220 | + |
221 | + # Error message should be shown. |
222 | + content = find_main_content(browser.contents) |
223 | + self.assertIn( |
224 | + "'messed up text' at line 2 is not a valid KEY=value pair.", |
225 | + extract_text(content)) |
226 | + |
227 | + # Assert that recipe still have the original build_args. |
228 | + login_person(self.person) |
229 | + IStore(recipe).reload(recipe) |
230 | + self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args) |
231 | + |
232 | def test_edit_with_invisible_processor(self): |
233 | # It's possible for existing recipes to have an enabled processor |
234 | # that's no longer usable with the current distroseries, which will |
235 | @@ -705,6 +766,37 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): |
236 | """ % (oci_project_name, oci_project_display), |
237 | self.getMainText(build.recipe)) |
238 | |
239 | + def test_index_with_build_args(self): |
240 | + oci_project = self.factory.makeOCIProject( |
241 | + pillar=self.distroseries.distribution) |
242 | + oci_project_name = oci_project.name |
243 | + oci_project_display = oci_project.display_name |
244 | + [ref] = self.factory.makeGitRefs( |
245 | + owner=self.person, target=self.person, name="recipe-repository", |
246 | + paths=["refs/heads/master"]) |
247 | + recipe = self.makeOCIRecipe( |
248 | + oci_project=oci_project, git_ref=ref, build_file="Dockerfile", |
249 | + build_args={"VAR1": "123", "VAR2": "XXX"}) |
250 | + build = self.makeBuild( |
251 | + recipe=recipe, status=BuildStatus.FULLYBUILT, |
252 | + duration=timedelta(minutes=30)) |
253 | + self.assertTextMatchesExpressionIgnoreWhitespace("""\ |
254 | + %s OCI project |
255 | + recipe-name |
256 | + .* |
257 | + OCI recipe information |
258 | + Owner: Test Person |
259 | + OCI project: %s |
260 | + Source: ~test-person/\\+git/recipe-repository:master |
261 | + Build file path: Dockerfile |
262 | + Build schedule: Built on request |
263 | + Build ARG: VAR1=123 VAR2=XXX |
264 | + Latest builds |
265 | + Status When complete Architecture |
266 | + Successfully built 30 minutes ago 386 |
267 | + """ % (oci_project_name, oci_project_display), |
268 | + self.getMainText(build.recipe)) |
269 | + |
270 | def test_index_success_with_buildlog(self): |
271 | # The build log is shown if it is there. |
272 | build = self.makeBuild( |
273 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
274 | index c433825..9e8edad 100644 |
275 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
276 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
277 | @@ -387,6 +387,13 @@ class IOCIRecipeEditableAttributes(IHasOwner): |
278 | required=True, |
279 | readonly=False)) |
280 | |
281 | + build_args = exported(Dict( |
282 | + title=_("Build ARG variables"), |
283 | + description=_("The dictionary of ARG variables to be used when " |
284 | + "building this recipe."), |
285 | + required=False, |
286 | + readonly=False)) |
287 | + |
288 | build_daily = exported(Bool( |
289 | title=_("Build daily"), |
290 | required=True, |
291 | @@ -433,7 +440,7 @@ class IOCIRecipeSet(Interface): |
292 | def new(name, registrant, owner, oci_project, git_ref, build_file, |
293 | description=None, official=False, require_virtualized=True, |
294 | build_daily=False, processors=None, date_created=DEFAULT, |
295 | - allow_internet=True): |
296 | + allow_internet=True, build_args=None): |
297 | """Create an IOCIRecipe.""" |
298 | |
299 | def exists(owner, oci_project, name): |
300 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
301 | index 2a83ccc..c3c2ad2 100644 |
302 | --- a/lib/lp/oci/model/ocirecipe.py |
303 | +++ b/lib/lp/oci/model/ocirecipe.py |
304 | @@ -14,6 +14,7 @@ __all__ = [ |
305 | |
306 | from lazr.lifecycle.event import ObjectCreatedEvent |
307 | import pytz |
308 | +from storm.databases.postgres import JSON |
309 | from storm.expr import ( |
310 | And, |
311 | Desc, |
312 | @@ -146,6 +147,8 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
313 | git_path = Unicode(name="git_path", allow_none=True) |
314 | build_file = Unicode(name="build_file", allow_none=False) |
315 | |
316 | + _build_args = JSON(name="build_args", allow_none=True) |
317 | + |
318 | require_virtualized = Bool(name="require_virtualized", default=True, |
319 | allow_none=False) |
320 | |
321 | @@ -156,7 +159,7 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
322 | def __init__(self, name, registrant, owner, oci_project, git_ref, |
323 | description=None, official=False, require_virtualized=True, |
324 | build_file=None, build_daily=False, date_created=DEFAULT, |
325 | - allow_internet=True): |
326 | + allow_internet=True, build_args=None): |
327 | if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE): |
328 | raise OCIRecipeFeatureDisabled() |
329 | super(OCIRecipe, self).__init__() |
330 | @@ -173,6 +176,7 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
331 | self.date_last_modified = date_created |
332 | self.git_ref = git_ref |
333 | self.allow_internet = allow_internet |
334 | + self.build_args = build_args or {} |
335 | |
336 | def __repr__(self): |
337 | return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % ( |
338 | @@ -188,6 +192,15 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
339 | """See `IOCIProject.setOfficialRecipe` method.""" |
340 | return self._official |
341 | |
342 | + @property |
343 | + def build_args(self): |
344 | + return self._build_args or {} |
345 | + |
346 | + @build_args.setter |
347 | + def build_args(self, value): |
348 | + assert value is None or isinstance(value, dict) |
349 | + self._build_args = {k: str(v) for k, v in (value or {}).items()} |
350 | + |
351 | def destroySelf(self): |
352 | """See `IOCIRecipe`.""" |
353 | # XXX twom 2019-11-26 This needs to expand as more build artifacts |
354 | @@ -538,7 +551,7 @@ class OCIRecipeSet: |
355 | def new(self, name, registrant, owner, oci_project, git_ref, build_file, |
356 | description=None, official=False, require_virtualized=True, |
357 | build_daily=False, processors=None, date_created=DEFAULT, |
358 | - allow_internet=True): |
359 | + allow_internet=True, build_args=None): |
360 | """See `IOCIRecipeSet`.""" |
361 | if not registrant.inTeam(owner): |
362 | if owner.is_team: |
363 | @@ -560,7 +573,7 @@ class OCIRecipeSet: |
364 | oci_recipe = OCIRecipe( |
365 | name, registrant, owner, oci_project, git_ref, description, |
366 | official, require_virtualized, build_file, build_daily, |
367 | - date_created, allow_internet) |
368 | + date_created, allow_internet, build_args) |
369 | store.add(oci_recipe) |
370 | |
371 | if processors is None: |
372 | diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py |
373 | index fce6dd3..1a5f78e 100644 |
374 | --- a/lib/lp/oci/model/ocirecipebuildbehaviour.py |
375 | +++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py |
376 | @@ -1,4 +1,4 @@ |
377 | -# Copyright 2019 Canonical Ltd. This software is licensed under the |
378 | +# Copyright 2019-2020 Canonical Ltd. This software is licensed under the |
379 | # GNU Affero General Public License version 3 (see the file LICENSE). |
380 | |
381 | """An `IBuildFarmJobBehaviour` for `OCIRecipeBuild`. |
382 | @@ -96,6 +96,7 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase): |
383 | logger=logger)) |
384 | |
385 | args['build_file'] = build.recipe.build_file |
386 | + args['build_args'] = build.recipe.build_args |
387 | |
388 | if build.recipe.git_ref is not None: |
389 | args["git_repository"] = ( |
390 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
391 | index 21a6939..2de9c29 100644 |
392 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
393 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
394 | @@ -64,6 +64,14 @@ |
395 | <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
396 | </dd> |
397 | </dl> |
398 | + <dl id="build-args" |
399 | + tal:define="build_args view/build_args" |
400 | + tal:condition="build_args"> |
401 | + <dt>Build ARG:</dt> |
402 | + <dd> |
403 | + <pre><span tal:replace="build_args"/></pre> |
404 | + </dd> |
405 | + </dl> |
406 | </div> |
407 | |
408 | <h2>Latest builds</h2> |
409 | diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt |
410 | index e5f2686..13c93c3 100644 |
411 | --- a/lib/lp/oci/templates/ocirecipe-new.pt |
412 | +++ b/lib/lp/oci/templates/ocirecipe-new.pt |
413 | @@ -32,6 +32,9 @@ |
414 | <tal:widget define="widget nocall:view/widgets/build_daily"> |
415 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
416 | </tal:widget> |
417 | + <tal:widget define="widget nocall:view/widgets/build_args"> |
418 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
419 | + </tal:widget> |
420 | <tal:widget define="widget nocall:view/widgets/processors"> |
421 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
422 | </tal:widget> |
423 | diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py |
424 | index ad67a7a..faa8862 100644 |
425 | --- a/lib/lp/oci/tests/test_ocirecipe.py |
426 | +++ b/lib/lp/oci/tests/test_ocirecipe.py |
427 | @@ -58,6 +58,7 @@ from lp.services.database.constants import ( |
428 | ONE_DAY_AGO, |
429 | UTC_NOW, |
430 | ) |
431 | +from lp.services.database.interfaces import IStore |
432 | from lp.services.database.sqlbase import flush_database_caches |
433 | from lp.services.features.testing import FeatureFixture |
434 | from lp.services.job.runner import JobRunner |
435 | @@ -664,6 +665,43 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): |
436 | [recipe3, recipe1, recipe2], |
437 | list(oci_project.searchRecipes(u"a"))) |
438 | |
439 | + def test_build_args_dict(self): |
440 | + args = {"MY_VERSION": "1.0.3", "ANOTHER_VERSION": "2.9.88"} |
441 | + recipe = self.factory.makeOCIRecipe(build_args=args) |
442 | + # Force fetch it from database |
443 | + store = IStore(recipe) |
444 | + store.invalidate(recipe) |
445 | + self.assertEqual(args, recipe.build_args) |
446 | + |
447 | + def test_build_args_not_dict(self): |
448 | + invalid_build_args_set = [ |
449 | + [1, 2, 3], |
450 | + "some string", |
451 | + 123, |
452 | + ] |
453 | + for invalid_build_args in invalid_build_args_set: |
454 | + self.assertRaises( |
455 | + AssertionError, self.factory.makeOCIRecipe, |
456 | + build_args=invalid_build_args) |
457 | + |
458 | + def test_build_args_flatten_dict(self): |
459 | + # Makes sure we only store one level of key=pair, flattening to |
460 | + # string every value. |
461 | + args = { |
462 | + "VAR1": {b"something": [1, 2, 3]}, |
463 | + "VAR2": 123, |
464 | + "VAR3": "A string", |
465 | + } |
466 | + recipe = self.factory.makeOCIRecipe(build_args=args) |
467 | + # Force fetch it from database |
468 | + store = IStore(recipe) |
469 | + store.invalidate(recipe) |
470 | + self.assertEqual({ |
471 | + "VAR1": "{'something': [1, 2, 3]}", |
472 | + "VAR2": "123", |
473 | + "VAR3": "A string", |
474 | + }, recipe.build_args) |
475 | + |
476 | |
477 | class TestOCIRecipeProcessors(TestCaseWithFactory): |
478 | |
479 | @@ -987,7 +1025,8 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
480 | oci_project = self.factory.makeOCIProject( |
481 | registrant=self.person) |
482 | recipe = self.factory.makeOCIRecipe( |
483 | - oci_project=oci_project) |
484 | + oci_project=oci_project, |
485 | + build_args={"VAR_A": "123"}) |
486 | url = api_url(recipe) |
487 | |
488 | ws_recipe = self.load_from_api(url) |
489 | @@ -1006,6 +1045,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
490 | git_ref_link=Equals(self.getAbsoluteURL(recipe.git_ref)), |
491 | description=Equals(recipe.description), |
492 | build_file=Equals(recipe.build_file), |
493 | + build_args=Equals({"VAR_A": "123"}), |
494 | build_daily=Equals(recipe.build_daily) |
495 | ))) |
496 | |
497 | @@ -1070,6 +1110,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
498 | "owner": person_url, |
499 | "git_ref": git_ref_url, |
500 | "build_file": "./Dockerfile", |
501 | + "build_args": {"VAR": "VAR VALUE"}, |
502 | "description": "My recipe"} |
503 | |
504 | resp = self.webservice.named_post(oci_project_url, "newRecipe", **obj) |
505 | @@ -1087,6 +1128,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
506 | description=Equals(obj["description"]), |
507 | owner_link=Equals(self.getAbsoluteURL(self.person)), |
508 | registrant_link=Equals(self.getAbsoluteURL(self.person)), |
509 | + build_args=Equals({"VAR": "VAR VALUE"}) |
510 | ))) |
511 | |
512 | def test_api_create_oci_recipe_non_legitimate_user(self): |
513 | diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py |
514 | index 3fdf8b4..e6247fe 100644 |
515 | --- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py |
516 | +++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py |
517 | @@ -111,6 +111,7 @@ class MakeOCIBuildMixin: |
518 | build = self.factory.makeOCIRecipeBuild( |
519 | recipe=recipe, **kwargs) |
520 | build.recipe.git_ref = git_ref |
521 | + build.recipe.build_args = {"BUILD_VAR": "123"} |
522 | |
523 | job = IBuildFarmJobBehaviour(build) |
524 | builder = MockBuilder() |
525 | @@ -242,6 +243,7 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory): |
526 | "archives": Equals(expected_archives), |
527 | "arch_tag": Equals("i386"), |
528 | "build_file": Equals(job.build.recipe.build_file), |
529 | + "build_args": Equals({"BUILD_VAR": "123"}), |
530 | "build_url": Equals(canonical_url(job.build)), |
531 | "fast_cleanup": Is(True), |
532 | "git_repository": Equals(ref.repository.git_https_url), |
533 | @@ -272,6 +274,7 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory): |
534 | "archives": Equals(expected_archives), |
535 | "arch_tag": Equals("i386"), |
536 | "build_file": Equals(job.build.recipe.build_file), |
537 | + "build_args": Equals({"BUILD_VAR": "123"}), |
538 | "build_url": Equals(canonical_url(job.build)), |
539 | "fast_cleanup": Is(True), |
540 | "git_repository": Equals(ref.repository.git_https_url), |
541 | diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py |
542 | index 2454662..e72a4cf 100644 |
543 | --- a/lib/lp/registry/interfaces/ociproject.py |
544 | +++ b/lib/lp/registry/interfaces/ociproject.py |
545 | @@ -33,6 +33,7 @@ from zope.interface import Interface |
546 | from zope.schema import ( |
547 | Bool, |
548 | Datetime, |
549 | + Dict, |
550 | Int, |
551 | Text, |
552 | TextLine, |
553 | @@ -164,6 +165,13 @@ class IOCIProjectLegitimate(Interface): |
554 | "branch that defines how to build the recipe."), |
555 | constraint=path_does_not_escape, |
556 | required=True), |
557 | + build_args=Dict( |
558 | + title=_("Build ARGs to be used when building the recipe"), |
559 | + description=_( |
560 | + "A dict of VARIABLE=VALUE to be used as ARG when building " |
561 | + "the recipe." |
562 | + ), |
563 | + required=False), |
564 | description=Text( |
565 | title=_("Description for this recipe."), |
566 | description=_("A short description of this recipe."), |
567 | @@ -174,7 +182,7 @@ class IOCIProjectLegitimate(Interface): |
568 | @operation_for_version("devel") |
569 | def newRecipe(name, registrant, owner, git_ref, build_file, |
570 | description=None, build_daily=False, |
571 | - require_virtualized=True): |
572 | + require_virtualized=True, build_args=None): |
573 | """Create an IOCIRecipe for this project.""" |
574 | |
575 | |
576 | diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py |
577 | index 28b3dc0..c27f2c6 100644 |
578 | --- a/lib/lp/registry/model/ociproject.py |
579 | +++ b/lib/lp/registry/model/ociproject.py |
580 | @@ -130,7 +130,7 @@ class OCIProject(BugTargetBase, StormBase): |
581 | |
582 | def newRecipe(self, name, registrant, owner, git_ref, |
583 | build_file, description=None, build_daily=False, |
584 | - require_virtualized=True): |
585 | + require_virtualized=True, build_args=None): |
586 | return getUtility(IOCIRecipeSet).new( |
587 | name=name, |
588 | registrant=registrant, |
589 | @@ -138,6 +138,7 @@ class OCIProject(BugTargetBase, StormBase): |
590 | oci_project=self, |
591 | git_ref=git_ref, |
592 | build_file=build_file, |
593 | + build_args=build_args, |
594 | description=description, |
595 | require_virtualized=require_virtualized, |
596 | build_daily=build_daily, |
597 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
598 | index efc5c57..a1de3ac 100644 |
599 | --- a/lib/lp/testing/factory.py |
600 | +++ b/lib/lp/testing/factory.py |
601 | @@ -4999,7 +4999,7 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
602 | oci_project=None, git_ref=None, description=None, |
603 | official=False, require_virtualized=True, |
604 | build_file=None, date_created=DEFAULT, |
605 | - allow_internet=True): |
606 | + allow_internet=True, build_args=None): |
607 | """Make a new OCIRecipe.""" |
608 | if name is None: |
609 | name = self.getUniqueString(u"oci-recipe-name") |
610 | @@ -5026,7 +5026,8 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
611 | official=official, |
612 | require_virtualized=require_virtualized, |
613 | date_created=date_created, |
614 | - allow_internet=allow_internet) |
615 | + allow_internet=allow_internet, |
616 | + build_args=build_args) |
617 | |
618 | def makeOCIRecipeArch(self, recipe=None, processor=None): |
619 | """Make a new OCIRecipeArch.""" |