Merge ~jugmac00/lpci:add-pre-post-run-hooks into lpci:main
- Git
- lp:~jugmac00/lpci
- add-pre-post-run-hooks
- Merge into main
Proposed by
Jürgen Gmach
Status: | Merged |
---|---|
Merged at revision: | 185c673ec63960aa05e9b0b7e8e6f44660c6f799 |
Proposed branch: | ~jugmac00/lpci:add-pre-post-run-hooks |
Merge into: | lpci:main |
Diff against target: |
374 lines (+174/-82) 6 files modified
NEWS.rst (+4/-1) docs/configuration.rst (+8/-0) lpcraft/commands/run.py (+144/-81) lpcraft/config.py (+2/-0) lpcraft/plugin/hookspecs.py (+12/-0) lpcraft/tests/test_config.py (+4/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email:
|
Commit message
This branch adds pre- and post- run hooks to jobs in lpcraft via the `run-before` and `run-after` configuration keys and the `lpcraft_
It also includes a slight refactor of the `run_job` logic to avoid code duplication.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/NEWS.rst b/NEWS.rst |
2 | index 4e89f28..bcec6ce 100644 |
3 | --- a/NEWS.rst |
4 | +++ b/NEWS.rst |
5 | @@ -5,7 +5,10 @@ Version history |
6 | 0.0.15 (unreleased) |
7 | =================== |
8 | |
9 | -- Nothing yet. |
10 | +- Allow ``run-before`` and ``run-after`` in ``.launchpad.yaml`` config. |
11 | + |
12 | +- Add ``lpcraft_execute_before_run`` and ``lpcraft_execute_after_run`` hooks. |
13 | + |
14 | |
15 | 0.0.14 (2022-05-18) |
16 | =================== |
17 | diff --git a/docs/configuration.rst b/docs/configuration.rst |
18 | index 5d16337..5e02c2b 100644 |
19 | --- a/docs/configuration.rst |
20 | +++ b/docs/configuration.rst |
21 | @@ -60,10 +60,18 @@ Job definitions |
22 | ``plugin`` (optional) |
23 | A plugin which will be used for this job. See :doc:`../plugins` |
24 | |
25 | +``run-before`` (optional) |
26 | + A string (possibly multi-line) containing shell commands to run for this |
27 | + job prior to the main ``run`` section. |
28 | + |
29 | ``run`` (optional) |
30 | A string (possibly multi-line) containing shell commands to run for this |
31 | job. |
32 | |
33 | +``run-after`` (optional) |
34 | + A string (possibly multi-line) containing shell commands to run for this |
35 | + job after the main ``run`` section. |
36 | + |
37 | ``output`` (optional) |
38 | See the :ref:`output-properties` section below. |
39 | |
40 | diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py |
41 | index 106ac2f..961b198 100644 |
42 | --- a/lpcraft/commands/run.py |
43 | +++ b/lpcraft/commands/run.py |
44 | @@ -9,12 +9,13 @@ import os |
45 | import shlex |
46 | from argparse import Namespace |
47 | from pathlib import Path, PurePath |
48 | -from typing import List, Optional, Set |
49 | +from typing import Dict, List, Optional, Set |
50 | |
51 | from craft_cli import EmitterMode, emit |
52 | -from craft_providers import Executor |
53 | +from craft_providers import Executor, lxd |
54 | from craft_providers.actions.snap_installer import install_from_store |
55 | from dotenv import dotenv_values |
56 | +from pluggy import PluginManager |
57 | |
58 | from lpcraft import env |
59 | from lpcraft.config import Config, Job, Output |
60 | @@ -193,6 +194,108 @@ def _copy_output_properties( |
61 | json.dump(properties, f) |
62 | |
63 | |
64 | +def _resolve_runtime_value( |
65 | + pm: PluginManager, job: Job, hook_name: str, job_property: str |
66 | +) -> Optional[str]: |
67 | + command_from_config: Optional[str] = getattr(job, job_property, None) |
68 | + if command_from_config is not None: |
69 | + return command_from_config |
70 | + rv: List[str] = getattr(pm.hook, hook_name)() |
71 | + return next(iter(rv), None) |
72 | + |
73 | + |
74 | +def _install_apt_packages( |
75 | + job_name: str, |
76 | + job: Job, |
77 | + packages: List[str], |
78 | + instance: lxd.LXDInstance, |
79 | + host_architecture: str, |
80 | + remote_cwd: Path, |
81 | + apt_replacement_repositories: Optional[List[str]], |
82 | + environment: Optional[Dict[str, Optional[str]]], |
83 | +) -> None: |
84 | + if apt_replacement_repositories: |
85 | + # replace sources.list |
86 | + lines = "\n".join(apt_replacement_repositories) + "\n" |
87 | + with emit.open_stream("Replacing /etc/apt/sources.list") as stream: |
88 | + instance.push_file_io( |
89 | + destination=PurePath("/etc/apt/sources.list"), |
90 | + content=io.BytesIO(lines.encode()), |
91 | + file_mode="0644", |
92 | + group="root", |
93 | + user="root", |
94 | + ) |
95 | + # update local repository information |
96 | + apt_update = ["apt", "update"] |
97 | + with emit.open_stream(f"Running {apt_update}") as stream: |
98 | + proc = instance.execute_run( |
99 | + apt_update, |
100 | + cwd=remote_cwd, |
101 | + env=environment, |
102 | + stdout=stream, |
103 | + stderr=stream, |
104 | + ) |
105 | + if proc.returncode != 0: |
106 | + raise CommandError( |
107 | + f"Job {job_name!r} for " |
108 | + f"{job.series}/{host_architecture} failed with " |
109 | + f"exit status {proc.returncode} " |
110 | + f"while running `{shlex.join(apt_update)}`.", |
111 | + retcode=proc.returncode, |
112 | + ) |
113 | + packages_cmd = ["apt", "install", "-y"] + packages |
114 | + emit.progress("Installing system packages") |
115 | + with emit.open_stream(f"Running {packages_cmd}") as stream: |
116 | + proc = instance.execute_run( |
117 | + packages_cmd, |
118 | + cwd=remote_cwd, |
119 | + env=environment, |
120 | + stdout=stream, |
121 | + stderr=stream, |
122 | + ) |
123 | + if proc.returncode != 0: |
124 | + raise CommandError( |
125 | + f"Job {job_name!r} for " |
126 | + f"{job.series}/{host_architecture} failed with " |
127 | + f"exit status {proc.returncode} " |
128 | + f"while running `{shlex.join(packages_cmd)}`.", |
129 | + retcode=proc.returncode, |
130 | + ) |
131 | + |
132 | + |
133 | +def _run_instance_command( |
134 | + command: str, |
135 | + job_name: str, |
136 | + job: Job, |
137 | + instance: lxd.LXDInstance, |
138 | + host_architecture: str, |
139 | + remote_cwd: Path, |
140 | + environment: Optional[Dict[str, Optional[str]]], |
141 | +) -> None: |
142 | + full_run_cmd = ["bash", "--noprofile", "--norc", "-ec", command] |
143 | + emit.progress("Running command for the job...") |
144 | + original_mode = emit.get_mode() |
145 | + if original_mode == EmitterMode.NORMAL: |
146 | + emit.set_mode(EmitterMode.VERBOSE) |
147 | + with emit.open_stream(f"Running {full_run_cmd}") as stream: |
148 | + proc = instance.execute_run( |
149 | + full_run_cmd, |
150 | + cwd=remote_cwd, |
151 | + env=environment, |
152 | + stdout=stream, |
153 | + stderr=stream, |
154 | + ) |
155 | + if original_mode == EmitterMode.NORMAL: |
156 | + emit.set_mode(original_mode) |
157 | + if proc.returncode != 0: |
158 | + raise CommandError( |
159 | + f"Job {job_name!r} for " |
160 | + f"{job.series}/{host_architecture} failed with " |
161 | + f"exit status {proc.returncode}.", |
162 | + retcode=proc.returncode, |
163 | + ) |
164 | + |
165 | + |
166 | def _run_job( |
167 | job_name: str, |
168 | job: Job, |
169 | @@ -208,15 +311,24 @@ def _run_job( |
170 | if host_architecture not in job.architectures: |
171 | return |
172 | pm = get_plugin_manager(job) |
173 | - # XXX jugmac00 2021-12-17: extract inferring run_command |
174 | - run_command = None |
175 | - |
176 | - run_from_configuration = job.run |
177 | - if run_from_configuration is not None: |
178 | - run_command = run_from_configuration |
179 | - else: |
180 | - rv = pm.hook.lpcraft_execute_run() |
181 | - run_command = rv and rv[0] or None |
182 | + pre_run_command = _resolve_runtime_value( |
183 | + pm, |
184 | + job, |
185 | + hook_name="lpcraft_execute_before_run", |
186 | + job_property="run_before", |
187 | + ) |
188 | + run_command = _resolve_runtime_value( |
189 | + pm, |
190 | + job, |
191 | + hook_name="lpcraft_execute_run", |
192 | + job_property="run", |
193 | + ) |
194 | + post_run_command = _resolve_runtime_value( |
195 | + pm, |
196 | + job, |
197 | + hook_name="lpcraft_execute_after_run", |
198 | + job_property="run_after", |
199 | + ) |
200 | |
201 | if not run_command: |
202 | raise CommandError( |
203 | @@ -265,77 +377,27 @@ def _run_job( |
204 | ) |
205 | packages = list(itertools.chain(*pm.hook.lpcraft_install_packages())) |
206 | if packages: |
207 | - if apt_replacement_repositories: |
208 | - # replace sources.list |
209 | - lines = "\n".join(apt_replacement_repositories) + "\n" |
210 | - with emit.open_stream( |
211 | - "Replacing /etc/apt/sources.list" |
212 | - ) as stream: |
213 | - instance.push_file_io( |
214 | - destination=PurePath("/etc/apt/sources.list"), |
215 | - content=io.BytesIO(lines.encode()), |
216 | - file_mode="0644", |
217 | - group="root", |
218 | - user="root", |
219 | - ) |
220 | - # update local repository information |
221 | - apt_update = ["apt", "update"] |
222 | - with emit.open_stream(f"Running {apt_update}") as stream: |
223 | - proc = instance.execute_run( |
224 | - apt_update, |
225 | - cwd=remote_cwd, |
226 | - env=environment, |
227 | - stdout=stream, |
228 | - stderr=stream, |
229 | - ) |
230 | - if proc.returncode != 0: |
231 | - raise CommandError( |
232 | - f"Job {job_name!r} for " |
233 | - f"{job.series}/{host_architecture} failed with " |
234 | - f"exit status {proc.returncode} " |
235 | - f"while running `{shlex.join(apt_update)}`.", |
236 | - retcode=proc.returncode, |
237 | - ) |
238 | - packages_cmd = ["apt", "install", "-y"] + packages |
239 | - emit.progress("Installing system packages") |
240 | - with emit.open_stream(f"Running {packages_cmd}") as stream: |
241 | - proc = instance.execute_run( |
242 | - packages_cmd, |
243 | - cwd=remote_cwd, |
244 | - env=environment, |
245 | - stdout=stream, |
246 | - stderr=stream, |
247 | - ) |
248 | - if proc.returncode != 0: |
249 | - raise CommandError( |
250 | - f"Job {job_name!r} for " |
251 | - f"{job.series}/{host_architecture} failed with " |
252 | - f"exit status {proc.returncode} " |
253 | - f"while running `{shlex.join(packages_cmd)}`.", |
254 | - retcode=proc.returncode, |
255 | - ) |
256 | - full_run_cmd = ["bash", "--noprofile", "--norc", "-ec", run_command] |
257 | - emit.progress("Running the job") |
258 | - original_mode = emit.get_mode() |
259 | - if original_mode == EmitterMode.NORMAL: |
260 | - emit.set_mode(EmitterMode.VERBOSE) |
261 | - with emit.open_stream(f"Running {full_run_cmd}") as stream: |
262 | - proc = instance.execute_run( |
263 | - full_run_cmd, |
264 | - cwd=remote_cwd, |
265 | - env=environment, |
266 | - stdout=stream, |
267 | - stderr=stream, |
268 | - ) |
269 | - if original_mode == EmitterMode.NORMAL: |
270 | - emit.set_mode(original_mode) |
271 | - if proc.returncode != 0: |
272 | - raise CommandError( |
273 | - f"Job {job_name!r} for " |
274 | - f"{job.series}/{host_architecture} failed with " |
275 | - f"exit status {proc.returncode}.", |
276 | - retcode=proc.returncode, |
277 | + _install_apt_packages( |
278 | + job_name=job_name, |
279 | + job=job, |
280 | + packages=packages, |
281 | + instance=instance, |
282 | + host_architecture=host_architecture, |
283 | + remote_cwd=remote_cwd, |
284 | + apt_replacement_repositories=apt_replacement_repositories, |
285 | + environment=environment, |
286 | ) |
287 | + for cmd in (pre_run_command, run_command, post_run_command): |
288 | + if cmd: |
289 | + _run_instance_command( |
290 | + command=cmd, |
291 | + job_name=job_name, |
292 | + job=job, |
293 | + instance=instance, |
294 | + host_architecture=host_architecture, |
295 | + remote_cwd=remote_cwd, |
296 | + environment=environment, |
297 | + ) |
298 | |
299 | if job.output is not None and output is not None: |
300 | target_path = output / job_name / job.series / host_architecture |
301 | @@ -401,6 +463,7 @@ def run(args: Namespace) -> int: |
302 | emit.error(e) |
303 | stage_failed = True |
304 | if stage_failed: |
305 | + # FIXME: should we still clean here? |
306 | raise CommandError( |
307 | f"Some jobs in {stage} failed; stopping.", retcode=1 |
308 | ) |
309 | diff --git a/lpcraft/config.py b/lpcraft/config.py |
310 | index 07f17ff..06f6e23 100644 |
311 | --- a/lpcraft/config.py |
312 | +++ b/lpcraft/config.py |
313 | @@ -67,7 +67,9 @@ class Job(ModelConfigDefaults): |
314 | |
315 | series: _Identifier |
316 | architectures: List[_Identifier] |
317 | + run_before: Optional[StrictStr] |
318 | run: Optional[StrictStr] |
319 | + run_after: Optional[StrictStr] |
320 | environment: Optional[Dict[str, Optional[str]]] |
321 | output: Optional[Output] |
322 | snaps: Optional[List[StrictStr]] |
323 | diff --git a/lpcraft/plugin/hookspecs.py b/lpcraft/plugin/hookspecs.py |
324 | index 6b198ea..ddb3b8a 100644 |
325 | --- a/lpcraft/plugin/hookspecs.py |
326 | +++ b/lpcraft/plugin/hookspecs.py |
327 | @@ -7,6 +7,8 @@ __all__ = [ |
328 | "lpcraft_install_packages", |
329 | "lpcraft_install_snaps", |
330 | "lpcraft_execute_run", |
331 | + "lpcraft_execute_before_run", |
332 | + "lpcraft_execute_after_run", |
333 | "lpcraft_set_environment", |
334 | ] |
335 | |
336 | @@ -41,3 +43,13 @@ def lpcraft_set_environment() -> dict[str, str | None]: |
337 | # Please note: when there is the same environment variable provided by |
338 | # the plugin and the configuration file, the one in the configuration |
339 | # file will be taken into account |
340 | + |
341 | + |
342 | +@hookspec # type: ignore |
343 | +def lpcraft_execute_before_run() -> str: |
344 | + """Command to execute prior to the main execution body.""" |
345 | + |
346 | + |
347 | +@hookspec # type: ignore |
348 | +def lpcraft_execute_after_run() -> str: |
349 | + """Command to execute after the main execution body.""" |
350 | diff --git a/lpcraft/tests/test_config.py b/lpcraft/tests/test_config.py |
351 | index 6867024..d34b718 100644 |
352 | --- a/lpcraft/tests/test_config.py |
353 | +++ b/lpcraft/tests/test_config.py |
354 | @@ -59,8 +59,10 @@ class TestConfig(TestCase): |
355 | test: |
356 | series: focal |
357 | architectures: [amd64, arm64] |
358 | + run-before: pip install --upgrade setuptools build |
359 | run: | |
360 | tox |
361 | + run-after: coverage report |
362 | """ |
363 | ) |
364 | ) |
365 | @@ -76,7 +78,9 @@ class TestConfig(TestCase): |
366 | MatchesStructure.byEquality( |
367 | series="focal", |
368 | architectures=["amd64", "arm64"], |
369 | + run_before="pip install --upgrade setuptools build", # noqa:E501 |
370 | run="tox\n", |
371 | + run_after="coverage report", |
372 | ) |
373 | ] |
374 | ) |
self-approving as the main parts were already improved and just a bit of documentation and changelog entries were added