Merge ~jugmac00/lpci:add-more-hooks into lpci:main
- Git
- lp:~jugmac00/lpci
- add-more-hooks
- Merge into main
Status: | Merged |
---|---|
Merged at revision: | f16ec90b5a97b9e324992884824b5b2edf00d0cf |
Proposed branch: | ~jugmac00/lpci:add-more-hooks |
Merge into: | lpci:main |
Diff against target: |
398 lines (+151/-37) 8 files modified
docs/plugins.rst (+5/-0) lpcraft/commands/run.py (+39/-16) lpcraft/commands/tests/test_run.py (+14/-14) lpcraft/config.py (+4/-0) lpcraft/plugin/hookspecs.py (+21/-0) lpcraft/plugin/lib.py (+6/-0) lpcraft/plugin/tests/test_plugins.py (+51/-7) lpcraft/plugins/tox.py (+11/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email:
|
Commit message
Add more hooks
Description of the change
-
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jürgen Gmach (jugmac00) wrote : | # |
Thanks for the review.
I certainly noticed that inferring configuration options within the run method may not the best option.
Do you recall my MP review on https:/
`_run_job(job_name, job, provider, getattr(args, "output", None))`
I think this is related to your comment here.
I am not (yet) convinced that Job is the right place to put the logic to load additional data from the plugins. I especially do not like that after executing `Config.
Currently, `class Config` is more like a representation of a YAML file rather than a full blown configuration object.
Basically we have several sources of configuration options, which we need to combine.
We need some kind of configuration builder, which encapsulates all that logic:
def generate_
a = load_yaml(...)
b = load_config_
c = load_config_
d = load_config_
# mix
return configuration
And I totally agree - this would be way easier to unit test.
But I think that is something left for 2022 :-)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Colin Watson (cjwatson) wrote : | # |
I'm not sure about every detail of this (e.g. to me `args.output` feels separate because it can't be set anywhere other than on the command line), but in broad terms I agree - we should certainly do some refactoring next year as time permits.
Preview Diff
1 | diff --git a/docs/plugins.rst b/docs/plugins.rst |
2 | index 4c644d6..ca26449 100644 |
3 | --- a/docs/plugins.rst |
4 | +++ b/docs/plugins.rst |
5 | @@ -20,3 +20,8 @@ Plugin discovery |
6 | |
7 | As currently only builtin plugins are supported, |
8 | you need to define a plugin in ``lpcraft/plugins/<plugin>``. |
9 | + |
10 | + |
11 | +.. comments |
12 | + |
13 | + XXX jugmac00 2021-12-17: render all available hooks via plugin |
14 | diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py |
15 | index 3c53eef..1805b05 100644 |
16 | --- a/lpcraft/commands/run.py |
17 | +++ b/lpcraft/commands/run.py |
18 | @@ -168,12 +168,36 @@ def _run_job( |
19 | host_architecture = get_host_architecture() |
20 | if host_architecture not in job.architectures: |
21 | return |
22 | - if job.run is None: |
23 | + pm = get_plugin_manager(job) |
24 | + # XXX jugmac00 2021-12-17: extract infering run_command |
25 | + run_command = None |
26 | + |
27 | + run_from_configuration = job.run |
28 | + if run_from_configuration is not None: |
29 | + run_command = run_from_configuration |
30 | + else: |
31 | + rv = pm.hook.lpcraft_execute_run() |
32 | + run_command = rv and rv[0] or None |
33 | + |
34 | + if not run_command: |
35 | raise CommandError( |
36 | f"Job {job_name!r} for {job.series}/{host_architecture} " |
37 | f"does not set 'run'" |
38 | ) |
39 | |
40 | + # XXX jugmac00 2021-12-17: extract infering environment variables |
41 | + rv = pm.hook.lpcraft_set_environment() |
42 | + if rv: |
43 | + # XXX jugmac00 2021-12-17: check for length or reduce? |
44 | + env_from_plugin = rv[0] |
45 | + else: |
46 | + env_from_plugin = {} |
47 | + |
48 | + env_from_configuration = job.environment |
49 | + if env_from_configuration is not None: |
50 | + env_from_plugin.update(env_from_configuration) |
51 | + environment = env_from_plugin |
52 | + |
53 | cwd = Path.cwd() |
54 | remote_cwd = env.get_managed_environment_project_path() |
55 | |
56 | @@ -186,16 +210,15 @@ def _run_job( |
57 | series=job.series, |
58 | architecture=host_architecture, |
59 | ) as instance: |
60 | - if job.snaps: |
61 | - for snap in job.snaps: |
62 | - emit.progress(f"Running `snap install {snap}`") |
63 | - install_from_store( |
64 | - executor=instance, |
65 | - snap_name=snap, |
66 | - channel="stable", |
67 | - classic=True, |
68 | - ) |
69 | - pm = get_plugin_manager(job) |
70 | + snaps = list(itertools.chain(*pm.hook.lpcraft_install_snaps())) |
71 | + for snap in snaps: |
72 | + emit.progress(f"Running `snap install {snap}`") |
73 | + install_from_store( |
74 | + executor=instance, |
75 | + snap_name=snap, |
76 | + channel="stable", |
77 | + classic=True, |
78 | + ) |
79 | packages = list(itertools.chain(*pm.hook.lpcraft_install_packages())) |
80 | if packages: |
81 | packages_cmd = ["apt", "install", "-y"] + packages |
82 | @@ -204,17 +227,17 @@ def _run_job( |
83 | proc = instance.execute_run( |
84 | packages_cmd, |
85 | cwd=remote_cwd, |
86 | - env=job.environment, |
87 | + env=environment, |
88 | stdout=stream, |
89 | stderr=stream, |
90 | ) |
91 | - run_cmd = ["bash", "--noprofile", "--norc", "-ec", job.run] |
92 | + full_run_cmd = ["bash", "--noprofile", "--norc", "-ec", run_command] |
93 | emit.progress("Running the job") |
94 | - with emit.open_stream(f"Running {run_cmd}") as stream: |
95 | + with emit.open_stream(f"Running {full_run_cmd}") as stream: |
96 | proc = instance.execute_run( |
97 | - run_cmd, |
98 | + full_run_cmd, |
99 | cwd=remote_cwd, |
100 | - env=job.environment, |
101 | + env=environment, |
102 | stdout=stream, |
103 | stderr=stream, |
104 | ) |
105 | diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py |
106 | index e682273..e9a93ea 100644 |
107 | --- a/lpcraft/commands/tests/test_run.py |
108 | +++ b/lpcraft/commands/tests/test_run.py |
109 | @@ -193,7 +193,7 @@ class TestRun(RunBaseTestCase): |
110 | call( |
111 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
112 | cwd=Path("/root/project"), |
113 | - env=None, |
114 | + env={}, |
115 | stdout=ANY, |
116 | stderr=ANY, |
117 | ) |
118 | @@ -281,7 +281,7 @@ class TestRun(RunBaseTestCase): |
119 | execute_run.assert_called_once_with( |
120 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
121 | cwd=Path("/root/project"), |
122 | - env=None, |
123 | + env={}, |
124 | stdout=ANY, |
125 | stderr=ANY, |
126 | ) |
127 | @@ -327,7 +327,7 @@ class TestRun(RunBaseTestCase): |
128 | call( |
129 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
130 | cwd=Path("/root/project"), |
131 | - env=None, |
132 | + env={}, |
133 | stdout=ANY, |
134 | stderr=ANY, |
135 | ), |
136 | @@ -340,7 +340,7 @@ class TestRun(RunBaseTestCase): |
137 | "pyproject-build", |
138 | ], |
139 | cwd=Path("/root/project"), |
140 | - env=None, |
141 | + env={}, |
142 | stdout=ANY, |
143 | stderr=ANY, |
144 | ), |
145 | @@ -392,14 +392,14 @@ class TestRun(RunBaseTestCase): |
146 | call( |
147 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
148 | cwd=Path("/root/project"), |
149 | - env=None, |
150 | + env={}, |
151 | stdout=ANY, |
152 | stderr=ANY, |
153 | ), |
154 | call( |
155 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
156 | cwd=Path("/root/project"), |
157 | - env=None, |
158 | + env={}, |
159 | stdout=ANY, |
160 | stderr=ANY, |
161 | ), |
162 | @@ -412,7 +412,7 @@ class TestRun(RunBaseTestCase): |
163 | "pyproject-build", |
164 | ], |
165 | cwd=Path("/root/project"), |
166 | - env=None, |
167 | + env={}, |
168 | stdout=ANY, |
169 | stderr=ANY, |
170 | ), |
171 | @@ -956,9 +956,9 @@ class TestRun(RunBaseTestCase): |
172 | call( |
173 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
174 | cwd=Path("/root/project"), |
175 | - env=None, |
176 | - stdout=27, |
177 | - stderr=27, |
178 | + env={}, |
179 | + stdout=ANY, |
180 | + stderr=ANY, |
181 | ), |
182 | ], |
183 | execute_run.call_args_list, |
184 | @@ -1003,14 +1003,14 @@ class TestRun(RunBaseTestCase): |
185 | "apache2", |
186 | ], |
187 | cwd=Path("/root/project"), |
188 | - env=None, |
189 | + env={}, |
190 | stdout=ANY, |
191 | stderr=ANY, |
192 | ), |
193 | call( |
194 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
195 | cwd=Path("/root/project"), |
196 | - env=None, |
197 | + env={}, |
198 | stdout=ANY, |
199 | stderr=ANY, |
200 | ), |
201 | @@ -1136,7 +1136,7 @@ class TestRunOne(RunBaseTestCase): |
202 | execute_run.assert_called_once_with( |
203 | ["bash", "--noprofile", "--norc", "-ec", "pyproject-build"], |
204 | cwd=Path("/root/project"), |
205 | - env=None, |
206 | + env={}, |
207 | stdout=ANY, |
208 | stderr=ANY, |
209 | ) |
210 | @@ -1184,7 +1184,7 @@ class TestRunOne(RunBaseTestCase): |
211 | execute_run.assert_called_once_with( |
212 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
213 | cwd=Path("/root/project"), |
214 | - env=None, |
215 | + env={}, |
216 | stdout=ANY, |
217 | stderr=ANY, |
218 | ) |
219 | diff --git a/lpcraft/config.py b/lpcraft/config.py |
220 | index ed6ae7f..98931f6 100644 |
221 | --- a/lpcraft/config.py |
222 | +++ b/lpcraft/config.py |
223 | @@ -60,6 +60,10 @@ class Output(ModelConfigDefaults): |
224 | class Job(ModelConfigDefaults): |
225 | """A job definition.""" |
226 | |
227 | + # XXX jugmac00 2012-12-17: working with Job's attributes could be |
228 | + # simplified if they would not be optional, but rather return e.g. |
229 | + # an empty list |
230 | + |
231 | series: _Identifier |
232 | architectures: List[_Identifier] |
233 | run: Optional[StrictStr] |
234 | diff --git a/lpcraft/plugin/hookspecs.py b/lpcraft/plugin/hookspecs.py |
235 | index f16f909..e1232f4 100644 |
236 | --- a/lpcraft/plugin/hookspecs.py |
237 | +++ b/lpcraft/plugin/hookspecs.py |
238 | @@ -11,3 +11,24 @@ hookspec = pluggy.HookspecMarker("lpcraft") |
239 | @hookspec # type: ignore |
240 | def lpcraft_install_packages() -> list[str]: |
241 | """system packages to be installed""" |
242 | + |
243 | + |
244 | +@hookspec # type: ignore |
245 | +def lpcraft_install_snaps() -> list[str]: |
246 | + """snaps to be installed""" |
247 | + |
248 | + |
249 | +@hookspec # type: ignore |
250 | +def lpcraft_execute_run() -> str: |
251 | + """command to be executed""" |
252 | + # Please note: when both a plugin and the configuration file are |
253 | + # providing a `run` command, the one from the configuration file will be |
254 | + # used |
255 | + |
256 | + |
257 | +@hookspec # type: ignore |
258 | +def lpcraft_set_environment() -> dict[str, str | None]: |
259 | + """environment variables to be set""" |
260 | + # Please note: when there is the same environment variable provided by |
261 | + # the plugin and the configuration file, the one in the configuration |
262 | + # file will be taken into account |
263 | diff --git a/lpcraft/plugin/lib.py b/lpcraft/plugin/lib.py |
264 | index b22be5a..ced3cdd 100644 |
265 | --- a/lpcraft/plugin/lib.py |
266 | +++ b/lpcraft/plugin/lib.py |
267 | @@ -23,3 +23,9 @@ class InternalPlugins: |
268 | if self.config.packages: |
269 | return self.config.packages |
270 | return [] |
271 | + |
272 | + @hookimpl # type: ignore |
273 | + def lpcraft_install_snaps(self) -> list[str]: |
274 | + if self.config.snaps: |
275 | + return self.config.snaps |
276 | + return [] |
277 | diff --git a/lpcraft/plugin/tests/test_builtin_plugins.py b/lpcraft/plugin/tests/test_plugins.py |
278 | similarity index 77% |
279 | rename from lpcraft/plugin/tests/test_builtin_plugins.py |
280 | rename to lpcraft/plugin/tests/test_plugins.py |
281 | index 79b0a1d..270ad7a 100644 |
282 | --- a/lpcraft/plugin/tests/test_builtin_plugins.py |
283 | +++ b/lpcraft/plugin/tests/test_plugins.py |
284 | @@ -14,7 +14,7 @@ from lpcraft.providers._lxd import LXDProvider, _LXDLauncher |
285 | from lpcraft.providers.tests import FakeLXDInstaller |
286 | |
287 | |
288 | -class TestBuiltinPlugins(CommandBaseTestCase): |
289 | +class TestPlugins(CommandBaseTestCase): |
290 | def makeLXDProvider( |
291 | self, |
292 | is_ready=True, |
293 | @@ -39,9 +39,6 @@ class TestBuiltinPlugins(CommandBaseTestCase): |
294 | mock_get_provider.return_value = provider |
295 | execute_run = launcher.return_value.execute_run |
296 | execute_run.return_value = subprocess.CompletedProcess([], 0) |
297 | - # XXX jugmac00 2021-12-16: the current implementation of the `tox` |
298 | - # plugin does not provide a run command, so we need to specify it |
299 | - # here in the configuration file |
300 | config = dedent( |
301 | """ |
302 | pipeline: |
303 | @@ -52,7 +49,6 @@ class TestBuiltinPlugins(CommandBaseTestCase): |
304 | series: focal |
305 | architectures: amd64 |
306 | packages: [nginx, apache2] |
307 | - run: tox |
308 | plugin: tox |
309 | """ |
310 | ) |
311 | @@ -65,14 +61,14 @@ class TestBuiltinPlugins(CommandBaseTestCase): |
312 | call( |
313 | ["apt", "install", "-y", "tox", "nginx", "apache2"], |
314 | cwd=PosixPath("/root/project"), |
315 | - env=None, |
316 | + env={"PLUGIN": "tox"}, |
317 | stdout=ANY, |
318 | stderr=ANY, |
319 | ), |
320 | call( |
321 | ["bash", "--noprofile", "--norc", "-ec", "tox"], |
322 | cwd=PosixPath("/root/project"), |
323 | - env=None, |
324 | + env={"PLUGIN": "tox"}, |
325 | stdout=ANY, |
326 | stderr=ANY, |
327 | ), |
328 | @@ -110,3 +106,51 @@ class TestBuiltinPlugins(CommandBaseTestCase): |
329 | |
330 | self.assertEqual(1, result.exit_code) |
331 | self.assertEqual([YAMLError("Unknown plugin")], result.errors) |
332 | + |
333 | + @patch("lpcraft.commands.run.get_provider") |
334 | + @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64") |
335 | + def test_run_command_from_configuration_takes_precedence( |
336 | + self, mock_get_host_architecture, mock_get_provider |
337 | + ): |
338 | + launcher = Mock(spec=launch) |
339 | + provider = self.makeLXDProvider(lxd_launcher=launcher) |
340 | + mock_get_provider.return_value = provider |
341 | + execute_run = launcher.return_value.execute_run |
342 | + execute_run.return_value = subprocess.CompletedProcess([], 0) |
343 | + config = dedent( |
344 | + """ |
345 | + pipeline: |
346 | + - test |
347 | + |
348 | + jobs: |
349 | + test: |
350 | + series: focal |
351 | + architectures: amd64 |
352 | + packages: [nginx, apache2] |
353 | + run: ls |
354 | + plugin: tox |
355 | + """ |
356 | + ) |
357 | + Path(".launchpad.yaml").write_text(config) |
358 | + |
359 | + self.run_command("run") |
360 | + |
361 | + self.assertEqual( |
362 | + [ |
363 | + call( |
364 | + ["apt", "install", "-y", "tox", "nginx", "apache2"], |
365 | + cwd=PosixPath("/root/project"), |
366 | + env={"PLUGIN": "tox"}, |
367 | + stdout=ANY, |
368 | + stderr=ANY, |
369 | + ), |
370 | + call( |
371 | + ["bash", "--noprofile", "--norc", "-ec", "ls"], |
372 | + cwd=PosixPath("/root/project"), |
373 | + env={"PLUGIN": "tox"}, |
374 | + stdout=ANY, |
375 | + stderr=ANY, |
376 | + ), |
377 | + ], |
378 | + execute_run.call_args_list, |
379 | + ) |
380 | diff --git a/lpcraft/plugins/tox.py b/lpcraft/plugins/tox.py |
381 | index 2952371..00ba66c 100644 |
382 | --- a/lpcraft/plugins/tox.py |
383 | +++ b/lpcraft/plugins/tox.py |
384 | @@ -15,3 +15,14 @@ class ToxPlugin: |
385 | @hookimpl # type: ignore |
386 | def lpcraft_install_packages(self) -> list[str]: |
387 | return ["tox"] |
388 | + |
389 | + @hookimpl # type: ignore |
390 | + def lpcraft_execute_run(self) -> str: |
391 | + return "tox" |
392 | + |
393 | + @hookimpl # type: ignore |
394 | + def lpcraft_set_environment(self) -> dict[str, str | None]: |
395 | + # XXX jugmac00 2021-12-17: this was added to raise coverage and is not |
396 | + # necessary. Let's remove this once we have a plugin which actually |
397 | + # needs to set environment variables. |
398 | + return {"PLUGIN": "tox"} |
Non-blocking, but I think this would be clearer and easier to unit-test if you pushed down the "combine plugin and configuration" logic for each of the various job properties to methods on `Job` (e.g. `Job.get_run`, or however you need to spell that to get it past pydantic), rather than open-coding it in `_run_job`.