Merge ~lgp171188/lpci:import-ppa-signing-keys into lpci:main
- Git
- lp:~lgp171188/lpci
- import-ppa-signing-keys
- Merge into main
Proposed by
Guruprasad
Status: | Merged |
---|---|
Merged at revision: | 3aadf9d13e88e67a46d9a4350de3ecb766fc9188 |
Proposed branch: | ~lgp171188/lpci:import-ppa-signing-keys |
Merge into: | lpci:main |
Prerequisite: | ~lgp171188/lpci:easier-way-to-add-a-ppa |
Diff against target: |
315 lines (+232/-1) 4 files modified
NEWS.rst (+3/-0) lpcraft/commands/run.py (+66/-1) lpcraft/commands/tests/test_run.py (+162/-0) tox.ini (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email:
|
Commit message
Import the signing key of a PPA automatically
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guruprasad (lgp171188) : | # |
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Colin Watson (cjwatson) : | # |
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 05b605f..fb9c5a2 100644 | |||
3 | --- a/NEWS.rst | |||
4 | +++ b/NEWS.rst | |||
5 | @@ -8,6 +8,9 @@ Version history | |||
6 | 8 | - Allow specifying PPAs using the shortform notation, | 8 | - Allow specifying PPAs using the shortform notation, |
7 | 9 | e.g. `ppa:launchpad/ubuntu/ppa`. | 9 | e.g. `ppa:launchpad/ubuntu/ppa`. |
8 | 10 | 10 | ||
9 | 11 | - Automatically import the signing keys for PPAs specified using | ||
10 | 12 | the short-form notation. | ||
11 | 13 | |||
12 | 11 | 0.0.37 (2022-12-09) | 14 | 0.0.37 (2022-12-09) |
13 | 12 | =================== | 15 | =================== |
14 | 13 | 16 | ||
15 | diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py | |||
16 | index d91bd41..72bc3a4 100644 | |||
17 | --- a/lpcraft/commands/run.py | |||
18 | +++ b/lpcraft/commands/run.py | |||
19 | @@ -7,11 +7,14 @@ import itertools | |||
20 | 7 | import json | 7 | import json |
21 | 8 | import os | 8 | import os |
22 | 9 | import shlex | 9 | import shlex |
23 | 10 | import subprocess | ||
24 | 11 | import tempfile | ||
25 | 10 | from argparse import ArgumentParser, Namespace | 12 | from argparse import ArgumentParser, Namespace |
26 | 11 | from pathlib import Path, PurePath | 13 | from pathlib import Path, PurePath |
27 | 12 | from tempfile import NamedTemporaryFile | 14 | from tempfile import NamedTemporaryFile |
28 | 13 | from typing import Dict, List, Optional, Set | 15 | from typing import Dict, List, Optional, Set |
29 | 14 | 16 | ||
30 | 17 | import requests | ||
31 | 15 | import yaml | 18 | import yaml |
32 | 16 | from craft_cli import BaseCommand, EmitterMode, emit | 19 | from craft_cli import BaseCommand, EmitterMode, emit |
33 | 17 | from craft_providers import Executor, lxd | 20 | from craft_providers import Executor, lxd |
34 | @@ -21,13 +24,23 @@ from jinja2 import BaseLoader, Environment | |||
35 | 21 | from pluggy import PluginManager | 24 | from pluggy import PluginManager |
36 | 22 | 25 | ||
37 | 23 | from lpcraft import env | 26 | from lpcraft import env |
39 | 24 | from lpcraft.config import Config, Input, Job, Output | 27 | from lpcraft.config import ( |
40 | 28 | Config, | ||
41 | 29 | Input, | ||
42 | 30 | Job, | ||
43 | 31 | Output, | ||
44 | 32 | PackageType, | ||
45 | 33 | PPAShortFormURL, | ||
46 | 34 | get_ppa_url_parts, | ||
47 | 35 | ) | ||
48 | 25 | from lpcraft.errors import CommandError | 36 | from lpcraft.errors import CommandError |
49 | 26 | from lpcraft.plugin.manager import get_plugin_manager | 37 | from lpcraft.plugin.manager import get_plugin_manager |
50 | 27 | from lpcraft.plugins import PLUGINS | 38 | from lpcraft.plugins import PLUGINS |
51 | 28 | from lpcraft.providers import Provider, get_provider | 39 | from lpcraft.providers import Provider, get_provider |
52 | 29 | from lpcraft.utils import get_host_architecture | 40 | from lpcraft.utils import get_host_architecture |
53 | 30 | 41 | ||
54 | 42 | LAUNCHPAD_API_BASE_URL = "https://api.launchpad.net/devel" | ||
55 | 43 | |||
56 | 31 | 44 | ||
57 | 32 | def _check_relative_path(path: PurePath, container: PurePath) -> PurePath: | 45 | def _check_relative_path(path: PurePath, container: PurePath) -> PurePath: |
58 | 33 | """Check that `path` does not escape `container`. | 46 | """Check that `path` does not escape `container`. |
59 | @@ -277,6 +290,47 @@ def _resolve_runtime_value( | |||
60 | 277 | return command_value | 290 | return command_value |
61 | 278 | 291 | ||
62 | 279 | 292 | ||
63 | 293 | def _import_signing_keys_for_ppas( | ||
64 | 294 | instance: lxd.LXDInstance, ppas: Set[PPAShortFormURL] | ||
65 | 295 | ) -> None: | ||
66 | 296 | for ppa in ppas: | ||
67 | 297 | owner, distribution, archive = get_ppa_url_parts(ppa) | ||
68 | 298 | signing_key_url = ( | ||
69 | 299 | f"{LAUNCHPAD_API_BASE_URL}/~{owner}/+archive/{distribution}" | ||
70 | 300 | f"/{archive}?ws.op=getSigningKeyData" | ||
71 | 301 | ) | ||
72 | 302 | response = requests.get(signing_key_url) | ||
73 | 303 | if not response.ok: | ||
74 | 304 | raise CommandError( | ||
75 | 305 | "Error retrieving the signing key for the" | ||
76 | 306 | f" '{owner}/{archive}/{distribution}' ppa." | ||
77 | 307 | " Please check if the PPA exists and is not empty." | ||
78 | 308 | ) | ||
79 | 309 | signing_key = response.json() | ||
80 | 310 | with NamedTemporaryFile("w+") as tmpfile: | ||
81 | 311 | tmpfile.write(signing_key) | ||
82 | 312 | tmpfile.flush() | ||
83 | 313 | with open(tmpfile.name) as keyfile: | ||
84 | 314 | gpg_cmd = [ | ||
85 | 315 | "gpg", | ||
86 | 316 | "--ignore-time-conflict", | ||
87 | 317 | "--no-options", | ||
88 | 318 | "--no-keyring", | ||
89 | 319 | ] | ||
90 | 320 | with tempfile.NamedTemporaryFile(mode="wb+") as keyring: | ||
91 | 321 | subprocess.check_call( | ||
92 | 322 | gpg_cmd + ["--dearmor"], stdin=keyfile, stdout=keyring | ||
93 | 323 | ) | ||
94 | 324 | os.fchmod(keyring.fileno(), 0o644) | ||
95 | 325 | instance.push_file( | ||
96 | 326 | destination=Path( | ||
97 | 327 | "/etc/apt/trusted.gpg.d" | ||
98 | 328 | f"/{owner}-{archive}-{distribution}.gpg" | ||
99 | 329 | ), | ||
100 | 330 | source=Path(keyring.name), | ||
101 | 331 | ) | ||
102 | 332 | |||
103 | 333 | |||
104 | 280 | def _install_apt_packages( | 334 | def _install_apt_packages( |
105 | 281 | job_name: str, | 335 | job_name: str, |
106 | 282 | job: Job, | 336 | job: Job, |
107 | @@ -313,6 +367,17 @@ def _install_apt_packages( | |||
108 | 313 | sources = template.render(**secrets) | 367 | sources = template.render(**secrets) |
109 | 314 | sources += "\n" | 368 | sources += "\n" |
110 | 315 | 369 | ||
111 | 370 | ppas = set() | ||
112 | 371 | for package_repository in job.package_repositories: | ||
113 | 372 | if ( | ||
114 | 373 | package_repository.type == PackageType.apt | ||
115 | 374 | and package_repository.ppa | ||
116 | 375 | ): | ||
117 | 376 | ppas.add(package_repository.ppa) | ||
118 | 377 | |||
119 | 378 | if ppas: | ||
120 | 379 | _import_signing_keys_for_ppas(instance, ppas) | ||
121 | 380 | |||
122 | 316 | with emit.open_stream("Replacing /etc/apt/sources.list") as stream: | 381 | with emit.open_stream("Replacing /etc/apt/sources.list") as stream: |
123 | 317 | instance.push_file_io( | 382 | instance.push_file_io( |
124 | 318 | destination=PurePath(sources_list_path), | 383 | destination=PurePath(sources_list_path), |
125 | diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py | |||
126 | index de8716a..df51380 100644 | |||
127 | --- a/lpcraft/commands/tests/test_run.py | |||
128 | +++ b/lpcraft/commands/tests/test_run.py | |||
129 | @@ -16,6 +16,7 @@ from craft_providers.lxd import launch | |||
130 | 16 | from fixtures import TempDir | 16 | from fixtures import TempDir |
131 | 17 | from testtools.matchers import MatchesStructure | 17 | from testtools.matchers import MatchesStructure |
132 | 18 | 18 | ||
133 | 19 | from lpcraft.commands.run import LAUNCHPAD_API_BASE_URL | ||
134 | 19 | from lpcraft.commands.tests import CommandBaseTestCase | 20 | from lpcraft.commands.tests import CommandBaseTestCase |
135 | 20 | from lpcraft.errors import CommandError, ConfigurationError | 21 | from lpcraft.errors import CommandError, ConfigurationError |
136 | 21 | from lpcraft.providers.tests import makeLXDProvider | 22 | from lpcraft.providers.tests import makeLXDProvider |
137 | @@ -484,6 +485,167 @@ class TestRun(RunBaseTestCase): | |||
138 | 484 | self.assertEqual("root", mock_info["group"]) | 485 | self.assertEqual("root", mock_info["group"]) |
139 | 485 | self.assertEqual("root", mock_info["user"]) | 486 | self.assertEqual("root", mock_info["user"]) |
140 | 486 | 487 | ||
141 | 488 | @responses.activate | ||
142 | 489 | @patch("lpcraft.commands.run.get_provider") | ||
143 | 490 | @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64") | ||
144 | 491 | def test_importing_ppa_key_key_not_found( | ||
145 | 492 | self, | ||
146 | 493 | mock_get_host_architecture, | ||
147 | 494 | mock_get_provider, | ||
148 | 495 | ): | ||
149 | 496 | launcher = Mock(spec=launch) | ||
150 | 497 | provider = makeLXDProvider(lxd_launcher=launcher) | ||
151 | 498 | mock_get_provider.return_value = provider | ||
152 | 499 | execute_run = launcher.return_value.execute_run | ||
153 | 500 | execute_run.return_value = subprocess.CompletedProcess([], 0) | ||
154 | 501 | |||
155 | 502 | responses.get( | ||
156 | 503 | "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL), | ||
157 | 504 | match=[ | ||
158 | 505 | responses.matchers.query_param_matcher( | ||
159 | 506 | {"ws.op": "getSigningKeyData"} | ||
160 | 507 | ) | ||
161 | 508 | ], | ||
162 | 509 | status=404, | ||
163 | 510 | ) | ||
164 | 511 | config = dedent( | ||
165 | 512 | """ | ||
166 | 513 | pipeline: | ||
167 | 514 | - test | ||
168 | 515 | jobs: | ||
169 | 516 | test: | ||
170 | 517 | series: focal | ||
171 | 518 | architectures: amd64 | ||
172 | 519 | run: ls -la | ||
173 | 520 | packages: [foo] | ||
174 | 521 | package-repositories: | ||
175 | 522 | - type: apt | ||
176 | 523 | ppa: example/foo | ||
177 | 524 | formats: [deb, deb-src] | ||
178 | 525 | suites: [focal] | ||
179 | 526 | """ | ||
180 | 527 | ) | ||
181 | 528 | Path(".launchpad.yaml").write_text(config) | ||
182 | 529 | |||
183 | 530 | result = self.run_command("run") | ||
184 | 531 | self.assertThat( | ||
185 | 532 | result, | ||
186 | 533 | MatchesStructure.byEquality( | ||
187 | 534 | exit_code=1, | ||
188 | 535 | errors=[ | ||
189 | 536 | CommandError( | ||
190 | 537 | "Error retrieving the signing key for the" | ||
191 | 538 | " 'example/foo/ubuntu' ppa. Please check" | ||
192 | 539 | " if the PPA exists and is not empty." | ||
193 | 540 | ) | ||
194 | 541 | ], | ||
195 | 542 | ), | ||
196 | 543 | ) | ||
197 | 544 | |||
198 | 545 | @responses.activate | ||
199 | 546 | @patch("lpcraft.commands.run.get_provider") | ||
200 | 547 | @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64") | ||
201 | 548 | def test_importing_ppa_signing_key( | ||
202 | 549 | self, | ||
203 | 550 | mock_get_host_architecture, | ||
204 | 551 | mock_get_provider, | ||
205 | 552 | ): | ||
206 | 553 | launcher = Mock(spec=launch) | ||
207 | 554 | provider = makeLXDProvider(lxd_launcher=launcher) | ||
208 | 555 | mock_get_provider.return_value = provider | ||
209 | 556 | execute_run = launcher.return_value.execute_run | ||
210 | 557 | execute_run.return_value = subprocess.CompletedProcess([], 0) | ||
211 | 558 | test_key = dedent( | ||
212 | 559 | """ | ||
213 | 560 | -----BEGIN PGP PUBLIC KEY BLOCK----- | ||
214 | 561 | Version: GnuPG v2 | ||
215 | 562 | |||
216 | 563 | mI0ESUm55wEEALrxow0PCnGeCAebH9g5+wtZBfXZdx2vZts+XsTTHxDRsMNgMC9b | ||
217 | 564 | 0klCgbydvkmF9WCphCjQ61Wp/Bh0C7DSXVCpA/xs55QB5VCUceIMZCbMTPq1h7Ht | ||
218 | 565 | cA1f+o6+OCPUntErG6eGize6kGhdjBNPOT+q4BSIL69rPuwfM9ZyAYcBABEBAAG0 | ||
219 | 566 | JkxhdW5jaHBhZCBQUEEgZm9yIExhdW5jaHBhZCBEZXZlbG9wZXJziLYEEwECACAF | ||
220 | 567 | AklJuecCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAtH/tsClF0rxsQA/0Q | ||
221 | 568 | w0Yk+xIA1xibyf+UCF9/4fXzdo/tr76qxPRyFiv0uLbFOmW6t26jzpWBHocCHcCU | ||
222 | 569 | 57l7rlcEzIHFMcS9Ol6MughP4lhywf9ceeqg2SD6AXjZ0iFarwkueTcHwff5j0lG | ||
223 | 570 | IzzCUVTYJ+m79f/r0dfctL2DwnX7JnT/41mEuR1qbokBHAQQAQIABgUCTB7s7wAK | ||
224 | 571 | CRDFXO8hUqH8T94pCACxl/Gdo82N01H82HvNBa8zQFixNQIwNJN/VxH3WfRvissW | ||
225 | 572 | OMTJnTnNOQErxUhqHrasvZf3djNoHeKRNToTTBaGiEwoySmEK05i4Toq74jWAOs6 | ||
226 | 573 | flD2S8natWbobK5V+B2pXZl5g/4Ay21C3H1sZlUxDCcOH9Jh8/0feAZHoSQ/V1Xa | ||
227 | 574 | rEPb+TGdV0hP3Yp7+nIT91sYkj566kA8fjoxJrY/EvXGn98bhYMbMNbtS1Z0WeGp | ||
228 | 575 | zG2hiL6wLSLBxz4Ae9MShOMwNyC1zmr/d1wlF0Efx1N9HaRtRq2s/zqH+ebB7Sr+ | ||
229 | 576 | V+SquObb0qr4eAjtslN5BxWROhf+wZM6WJO0Z6nBiQEcBBABAgAGBQJTHvsiAAoJ | ||
230 | 577 | EIngjfAzAr5Z8y4H/jltxz5OwHIDoiXsyWnpjO1SZUV6I6evKpSD7huYtd7MwFZC | ||
231 | 578 | 0CgExsPPqLNQCUxITR+9jlqofi/QsTwP7Qq55VmIrKLrZ9KCK1qBnMa/YEXi6TeK | ||
232 | 579 | 65lnyN6lNOdzhcsBm3s1/U9ewWp1vsw4UAclmu6tI8GUko+e32K1QjMtIjeVejQl | ||
233 | 580 | JCYDjuxfHhcFWyRo0TWu24F6VD3YxBHpne/M00yd2mLLpHdQrxw/vbvVhZkRDutQ | ||
234 | 581 | emKRA81ZM2WZ1iqYOXtEs5VrD/PtU0nvSAowgeWBmcOwWn3Om+pVsnSoFo46CDvo | ||
235 | 582 | C6YXOWMOMFIxfVhPWqlBkWQsnXFzgk/Xyo4vlTY==Wq6H | ||
236 | 583 | -----END PGP PUBLIC KEY BLOCK----- | ||
237 | 584 | """ | ||
238 | 585 | ) | ||
239 | 586 | test_key = json.dumps(test_key) | ||
240 | 587 | responses.get( | ||
241 | 588 | "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL), | ||
242 | 589 | match=[ | ||
243 | 590 | responses.matchers.query_param_matcher( | ||
244 | 591 | {"ws.op": "getSigningKeyData"} | ||
245 | 592 | ) | ||
246 | 593 | ], | ||
247 | 594 | body=test_key, | ||
248 | 595 | ) | ||
249 | 596 | responses.get( | ||
250 | 597 | "{}/~example/+archive/debian/bar".format(LAUNCHPAD_API_BASE_URL), | ||
251 | 598 | match=[ | ||
252 | 599 | responses.matchers.query_param_matcher( | ||
253 | 600 | {"ws.op": "getSigningKeyData"} | ||
254 | 601 | ) | ||
255 | 602 | ], | ||
256 | 603 | body=test_key, | ||
257 | 604 | ) | ||
258 | 605 | config = dedent( | ||
259 | 606 | """ | ||
260 | 607 | pipeline: | ||
261 | 608 | - test | ||
262 | 609 | jobs: | ||
263 | 610 | test: | ||
264 | 611 | series: focal | ||
265 | 612 | architectures: amd64 | ||
266 | 613 | run: ls -la | ||
267 | 614 | packages: [foo] | ||
268 | 615 | package-repositories: | ||
269 | 616 | - type: apt | ||
270 | 617 | ppa: example/foo | ||
271 | 618 | formats: [deb, deb-src] | ||
272 | 619 | suites: [focal] | ||
273 | 620 | - type: apt | ||
274 | 621 | ppa: example/debian/bar | ||
275 | 622 | formats: [deb, deb-src] | ||
276 | 623 | suites: [focal] | ||
277 | 624 | """ | ||
278 | 625 | ) | ||
279 | 626 | Path(".launchpad.yaml").write_text(config) | ||
280 | 627 | |||
281 | 628 | self.run_command("run") | ||
282 | 629 | mock_push_file = launcher.return_value.push_file | ||
283 | 630 | self.assertEqual(2, mock_push_file.call_count) | ||
284 | 631 | mock_push_file.assert_has_calls( | ||
285 | 632 | [ | ||
286 | 633 | call( | ||
287 | 634 | destination=Path( | ||
288 | 635 | "/etc/apt/trusted.gpg.d/example-foo-ubuntu.gpg" | ||
289 | 636 | ), | ||
290 | 637 | source=ANY, | ||
291 | 638 | ), | ||
292 | 639 | call( | ||
293 | 640 | destination=Path( | ||
294 | 641 | "/etc/apt/trusted.gpg.d/example-bar-debian.gpg" | ||
295 | 642 | ), | ||
296 | 643 | source=ANY, | ||
297 | 644 | ), | ||
298 | 645 | ], | ||
299 | 646 | any_order=True, | ||
300 | 647 | ) | ||
301 | 648 | |||
302 | 487 | @patch("lpcraft.commands.run.get_provider") | 649 | @patch("lpcraft.commands.run.get_provider") |
303 | 488 | @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64") | 650 | @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64") |
304 | 489 | def test_updating_package_info_fails( | 651 | def test_updating_package_info_fails( |
305 | diff --git a/tox.ini b/tox.ini | |||
306 | index 1d9be16..409c12f 100644 | |||
307 | --- a/tox.ini | |||
308 | +++ b/tox.ini | |||
309 | @@ -52,6 +52,7 @@ deps = | |||
310 | 52 | .[test] | 52 | .[test] |
311 | 53 | mypy | 53 | mypy |
312 | 54 | types-PyYAML | 54 | types-PyYAML |
313 | 55 | types-requests | ||
314 | 55 | commands = | 56 | commands = |
315 | 56 | mypy --cache-dir="{envdir}/mypy_cache" --strict {posargs:lpcraft} | 57 | mypy --cache-dir="{envdir}/mypy_cache" --strict {posargs:lpcraft} |
316 | 57 | 58 |