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

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 1d8dfd87f188618cd5d841a0709e71dcd16faa16
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:ocirecipe-args
Merge into: launchpad:master
Diff against target: 320 lines (+90/-11)
8 files modified
lib/lp/oci/interfaces/ocirecipe.py (+10/-1)
lib/lp/oci/model/ocirecipe.py (+17/-3)
lib/lp/oci/model/ocirecipebuildbehaviour.py (+2/-1)
lib/lp/oci/tests/test_ocirecipe.py (+44/-2)
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)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+389754@code.launchpad.net

Commit message

Adding OCIRecipe.build_args to allow ARG (--build-arg) options when building OCI images

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :
~pappacena/launchpad:ocirecipe-args updated
7e10963... by Thiago F. Pappacena

Changing OCIRecipe.build_args datatype to jsonb

9f1937d... by Thiago F. Pappacena

Merge branch 'master' into ocirecipe-args

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
~pappacena/launchpad:ocirecipe-args updated
3680244... by Thiago F. Pappacena

py3 compat and better IOCIRecipe.build_args definition

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

Pushed the requested changes.

~pappacena/launchpad:ocirecipe-args updated
1d8dfd8... by Thiago F. Pappacena

Merge branch 'master' into ocirecipe-args

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
2index e185ea5..718cdd3 100644
3--- a/lib/lp/oci/interfaces/ocirecipe.py
4+++ b/lib/lp/oci/interfaces/ocirecipe.py
5@@ -388,6 +388,15 @@ class IOCIRecipeEditableAttributes(IHasOwner):
6 required=True,
7 readonly=False))
8
9+ build_args = exported(Dict(
10+ title=_("Build ARG variables"),
11+ description=_("The dictionary of ARG variables to be used when "
12+ "building this recipe."),
13+ key_type=TextLine(title=_("ARG name")),
14+ value_type=TextLine(title=_("ARG value")),
15+ required=False,
16+ readonly=False))
17+
18 build_path = exported(TextLine(
19 title=_("Build directory context"),
20 description=_("Directory to use for build context "
21@@ -442,7 +451,7 @@ class IOCIRecipeSet(Interface):
22 def new(name, registrant, owner, oci_project, git_ref, build_file,
23 description=None, official=False, require_virtualized=True,
24 build_daily=False, processors=None, date_created=DEFAULT,
25- allow_internet=True):
26+ allow_internet=True, build_args=None):
27 """Create an IOCIRecipe."""
28
29 def exists(owner, oci_project, name):
30diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
31index ea03bf4..3ca817c 100644
32--- a/lib/lp/oci/model/ocirecipe.py
33+++ b/lib/lp/oci/model/ocirecipe.py
34@@ -14,6 +14,8 @@ __all__ = [
35
36 from lazr.lifecycle.event import ObjectCreatedEvent
37 import pytz
38+import six
39+from storm.databases.postgres import JSON
40 from storm.expr import (
41 And,
42 Desc,
43@@ -146,6 +148,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
44 git_path = Unicode(name="git_path", allow_none=True)
45 build_file = Unicode(name="build_file", allow_none=False)
46 build_path = Unicode(name="build_path", allow_none=False)
47+ _build_args = JSON(name="build_args", allow_none=True)
48
49 require_virtualized = Bool(name="require_virtualized", default=True,
50 allow_none=False)
51@@ -157,7 +160,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
52 def __init__(self, name, registrant, owner, oci_project, git_ref,
53 description=None, official=False, require_virtualized=True,
54 build_file=None, build_daily=False, date_created=DEFAULT,
55- allow_internet=True, build_path=None):
56+ allow_internet=True, build_args=None, build_path=None):
57 if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
58 raise OCIRecipeFeatureDisabled()
59 super(OCIRecipe, self).__init__()
60@@ -174,6 +177,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
61 self.date_last_modified = date_created
62 self.git_ref = git_ref
63 self.allow_internet = allow_internet
64+ self.build_args = build_args or {}
65 self.build_path = build_path
66
67 def __repr__(self):
68@@ -190,6 +194,16 @@ class OCIRecipe(Storm, WebhookTargetMixin):
69 """See `IOCIProject.setOfficialRecipe` method."""
70 return self._official
71
72+ @property
73+ def build_args(self):
74+ return self._build_args or {}
75+
76+ @build_args.setter
77+ def build_args(self, value):
78+ assert value is None or isinstance(value, dict)
79+ self._build_args = {k: six.text_type(v)
80+ for k, v in (value or {}).items()}
81+
82 def destroySelf(self):
83 """See `IOCIRecipe`."""
84 # XXX twom 2019-11-26 This needs to expand as more build artifacts
85@@ -540,7 +554,7 @@ class OCIRecipeSet:
86 def new(self, name, registrant, owner, oci_project, git_ref, build_file,
87 description=None, official=False, require_virtualized=True,
88 build_daily=False, processors=None, date_created=DEFAULT,
89- allow_internet=True, build_path=None):
90+ allow_internet=True, build_args=None, build_path=None):
91 """See `IOCIRecipeSet`."""
92 if not registrant.inTeam(owner):
93 if owner.is_team:
94@@ -565,7 +579,7 @@ class OCIRecipeSet:
95 oci_recipe = OCIRecipe(
96 name, registrant, owner, oci_project, git_ref, description,
97 official, require_virtualized, build_file, build_daily,
98- date_created, allow_internet, build_path)
99+ date_created, allow_internet, build_args, build_path)
100 store.add(oci_recipe)
101
102 if processors is None:
103diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
104index 844a2c8..742c8a2 100644
105--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
106+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
107@@ -1,4 +1,4 @@
108-# Copyright 2019 Canonical Ltd. This software is licensed under the
109+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
110 # GNU Affero General Public License version 3 (see the file LICENSE).
111
112 """An `IBuildFarmJobBehaviour` for `OCIRecipeBuild`.
113@@ -96,6 +96,7 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
114 logger=logger))
115
116 args['build_file'] = build.recipe.build_file
117+ args['build_args'] = build.recipe.build_args
118 args['build_path'] = build.recipe.build_path
119
120 if build.recipe.git_ref is not None:
121diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
122index 30c547f..76997a7 100644
123--- a/lib/lp/oci/tests/test_ocirecipe.py
124+++ b/lib/lp/oci/tests/test_ocirecipe.py
125@@ -62,6 +62,7 @@ from lp.services.database.constants import (
126 ONE_DAY_AGO,
127 UTC_NOW,
128 )
129+from lp.services.database.interfaces import IStore
130 from lp.services.database.sqlbase import flush_database_caches
131 from lp.services.features.testing import FeatureFixture
132 from lp.services.job.runner import JobRunner
133@@ -700,6 +701,43 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
134 [recipe3, recipe1, recipe2],
135 list(oci_project.searchRecipes(u"a")))
136
137+ def test_build_args_dict(self):
138+ args = {"MY_VERSION": "1.0.3", "ANOTHER_VERSION": "2.9.88"}
139+ recipe = self.factory.makeOCIRecipe(build_args=args)
140+ # Force fetch it from database
141+ store = IStore(recipe)
142+ store.invalidate(recipe)
143+ self.assertEqual(args, recipe.build_args)
144+
145+ def test_build_args_not_dict(self):
146+ invalid_build_args_set = [
147+ [1, 2, 3],
148+ "some string",
149+ 123,
150+ ]
151+ for invalid_build_args in invalid_build_args_set:
152+ self.assertRaises(
153+ AssertionError, self.factory.makeOCIRecipe,
154+ build_args=invalid_build_args)
155+
156+ def test_build_args_flatten_dict(self):
157+ # Makes sure we only store one level of key=pair, flattening to
158+ # string every value.
159+ args = {
160+ "VAR1": {b"something": [1, 2, 3]},
161+ "VAR2": 123,
162+ "VAR3": "A string",
163+ }
164+ recipe = self.factory.makeOCIRecipe(build_args=args)
165+ # Force fetch it from database
166+ store = IStore(recipe)
167+ store.invalidate(recipe)
168+ self.assertEqual({
169+ "VAR1": "{'something': [1, 2, 3]}",
170+ "VAR2": "123",
171+ "VAR3": "A string",
172+ }, recipe.build_args)
173+
174
175 class TestOCIRecipeProcessors(TestCaseWithFactory):
176
177@@ -1024,7 +1062,8 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
178 oci_project = self.factory.makeOCIProject(
179 registrant=self.person)
180 recipe = self.factory.makeOCIRecipe(
181- oci_project=oci_project)
182+ oci_project=oci_project,
183+ build_args={"VAR_A": "123"})
184 url = api_url(recipe)
185
186 ws_recipe = self.load_from_api(url)
187@@ -1043,8 +1082,9 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
188 git_ref_link=Equals(self.getAbsoluteURL(recipe.git_ref)),
189 description=Equals(recipe.description),
190 build_file=Equals(recipe.build_file),
191+ build_args=Equals({"VAR_A": "123"}),
192 build_daily=Equals(recipe.build_daily),
193- build_path=Equals(recipe.build_path)
194+ build_path=Equals(recipe.build_path),
195 )))
196
197 def test_api_patch_oci_recipe(self):
198@@ -1108,6 +1148,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
199 "owner": person_url,
200 "git_ref": git_ref_url,
201 "build_file": "./Dockerfile",
202+ "build_args": {"VAR": "VAR VALUE"},
203 "description": "My recipe"}
204
205 resp = self.webservice.named_post(oci_project_url, "newRecipe", **obj)
206@@ -1125,6 +1166,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
207 description=Equals(obj["description"]),
208 owner_link=Equals(self.getAbsoluteURL(self.person)),
209 registrant_link=Equals(self.getAbsoluteURL(self.person)),
210+ build_args=Equals({"VAR": "VAR VALUE"})
211 )))
212
213 def test_api_create_oci_recipe_non_legitimate_user(self):
214diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
215index bbc1ee6..6397405 100644
216--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
217+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
218@@ -111,6 +111,7 @@ class MakeOCIBuildMixin:
219 build = self.factory.makeOCIRecipeBuild(
220 recipe=recipe, **kwargs)
221 build.recipe.git_ref = git_ref
222+ build.recipe.build_args = {"BUILD_VAR": "123"}
223
224 job = IBuildFarmJobBehaviour(build)
225 builder = MockBuilder()
226@@ -242,6 +243,7 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory):
227 "archives": Equals(expected_archives),
228 "arch_tag": Equals("i386"),
229 "build_file": Equals(job.build.recipe.build_file),
230+ "build_args": Equals({"BUILD_VAR": "123"}),
231 "build_path": Equals(job.build.recipe.build_path),
232 "build_url": Equals(canonical_url(job.build)),
233 "fast_cleanup": Is(True),
234@@ -273,6 +275,7 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory):
235 "archives": Equals(expected_archives),
236 "arch_tag": Equals("i386"),
237 "build_file": Equals(job.build.recipe.build_file),
238+ "build_args": Equals({"BUILD_VAR": "123"}),
239 "build_path": Equals(job.build.recipe.build_path),
240 "build_url": Equals(canonical_url(job.build)),
241 "fast_cleanup": Is(True),
242diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
243index 2454662..e72a4cf 100644
244--- a/lib/lp/registry/interfaces/ociproject.py
245+++ b/lib/lp/registry/interfaces/ociproject.py
246@@ -33,6 +33,7 @@ from zope.interface import Interface
247 from zope.schema import (
248 Bool,
249 Datetime,
250+ Dict,
251 Int,
252 Text,
253 TextLine,
254@@ -164,6 +165,13 @@ class IOCIProjectLegitimate(Interface):
255 "branch that defines how to build the recipe."),
256 constraint=path_does_not_escape,
257 required=True),
258+ build_args=Dict(
259+ title=_("Build ARGs to be used when building the recipe"),
260+ description=_(
261+ "A dict of VARIABLE=VALUE to be used as ARG when building "
262+ "the recipe."
263+ ),
264+ required=False),
265 description=Text(
266 title=_("Description for this recipe."),
267 description=_("A short description of this recipe."),
268@@ -174,7 +182,7 @@ class IOCIProjectLegitimate(Interface):
269 @operation_for_version("devel")
270 def newRecipe(name, registrant, owner, git_ref, build_file,
271 description=None, build_daily=False,
272- require_virtualized=True):
273+ require_virtualized=True, build_args=None):
274 """Create an IOCIRecipe for this project."""
275
276
277diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
278index 28b3dc0..c27f2c6 100644
279--- a/lib/lp/registry/model/ociproject.py
280+++ b/lib/lp/registry/model/ociproject.py
281@@ -130,7 +130,7 @@ class OCIProject(BugTargetBase, StormBase):
282
283 def newRecipe(self, name, registrant, owner, git_ref,
284 build_file, description=None, build_daily=False,
285- require_virtualized=True):
286+ require_virtualized=True, build_args=None):
287 return getUtility(IOCIRecipeSet).new(
288 name=name,
289 registrant=registrant,
290@@ -138,6 +138,7 @@ class OCIProject(BugTargetBase, StormBase):
291 oci_project=self,
292 git_ref=git_ref,
293 build_file=build_file,
294+ build_args=build_args,
295 description=description,
296 require_virtualized=require_virtualized,
297 build_daily=build_daily,
298diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
299index d469c5a..57babc5 100644
300--- a/lib/lp/testing/factory.py
301+++ b/lib/lp/testing/factory.py
302@@ -4961,7 +4961,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
303 oci_project=None, git_ref=None, description=None,
304 official=False, require_virtualized=True,
305 build_file=None, date_created=DEFAULT,
306- allow_internet=True, build_path=None):
307+ allow_internet=True, build_args=None, build_path=None):
308 """Make a new OCIRecipe."""
309 if name is None:
310 name = self.getUniqueString(u"oci-recipe-name")
311@@ -4991,7 +4991,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
312 official=official,
313 require_virtualized=require_virtualized,
314 date_created=date_created,
315- allow_internet=allow_internet)
316+ allow_internet=allow_internet,
317+ build_args=build_args)
318
319 def makeOCIRecipeArch(self, recipe=None, processor=None):
320 """Make a new OCIRecipeArch."""