Merge ~lgp171188/lpci:import-ppa-signing-keys into lpci: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)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+433733@code.launchpad.net

Commit message

Import the signing key of a PPA automatically

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/NEWS.rst b/NEWS.rst
index 05b605f..fb9c5a2 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -8,6 +8,9 @@ Version history
8- Allow specifying PPAs using the shortform notation,8- Allow specifying PPAs using the shortform notation,
9 e.g. `ppa:launchpad/ubuntu/ppa`.9 e.g. `ppa:launchpad/ubuntu/ppa`.
1010
11- Automatically import the signing keys for PPAs specified using
12 the short-form notation.
13
110.0.37 (2022-12-09)140.0.37 (2022-12-09)
12===================15===================
1316
diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
index d91bd41..72bc3a4 100644
--- a/lpcraft/commands/run.py
+++ b/lpcraft/commands/run.py
@@ -7,11 +7,14 @@ import itertools
7import json7import json
8import os8import os
9import shlex9import shlex
10import subprocess
11import tempfile
10from argparse import ArgumentParser, Namespace12from argparse import ArgumentParser, Namespace
11from pathlib import Path, PurePath13from pathlib import Path, PurePath
12from tempfile import NamedTemporaryFile14from tempfile import NamedTemporaryFile
13from typing import Dict, List, Optional, Set15from typing import Dict, List, Optional, Set
1416
17import requests
15import yaml18import yaml
16from craft_cli import BaseCommand, EmitterMode, emit19from craft_cli import BaseCommand, EmitterMode, emit
17from craft_providers import Executor, lxd20from craft_providers import Executor, lxd
@@ -21,13 +24,23 @@ from jinja2 import BaseLoader, Environment
21from pluggy import PluginManager24from pluggy import PluginManager
2225
23from lpcraft import env26from lpcraft import env
24from lpcraft.config import Config, Input, Job, Output27from lpcraft.config import (
28 Config,
29 Input,
30 Job,
31 Output,
32 PackageType,
33 PPAShortFormURL,
34 get_ppa_url_parts,
35)
25from lpcraft.errors import CommandError36from lpcraft.errors import CommandError
26from lpcraft.plugin.manager import get_plugin_manager37from lpcraft.plugin.manager import get_plugin_manager
27from lpcraft.plugins import PLUGINS38from lpcraft.plugins import PLUGINS
28from lpcraft.providers import Provider, get_provider39from lpcraft.providers import Provider, get_provider
29from lpcraft.utils import get_host_architecture40from lpcraft.utils import get_host_architecture
3041
42LAUNCHPAD_API_BASE_URL = "https://api.launchpad.net/devel"
43
3144
32def _check_relative_path(path: PurePath, container: PurePath) -> PurePath:45def _check_relative_path(path: PurePath, container: PurePath) -> PurePath:
33 """Check that `path` does not escape `container`.46 """Check that `path` does not escape `container`.
@@ -277,6 +290,47 @@ def _resolve_runtime_value(
277 return command_value290 return command_value
278291
279292
293def _import_signing_keys_for_ppas(
294 instance: lxd.LXDInstance, ppas: Set[PPAShortFormURL]
295) -> None:
296 for ppa in ppas:
297 owner, distribution, archive = get_ppa_url_parts(ppa)
298 signing_key_url = (
299 f"{LAUNCHPAD_API_BASE_URL}/~{owner}/+archive/{distribution}"
300 f"/{archive}?ws.op=getSigningKeyData"
301 )
302 response = requests.get(signing_key_url)
303 if not response.ok:
304 raise CommandError(
305 "Error retrieving the signing key for the"
306 f" '{owner}/{archive}/{distribution}' ppa."
307 " Please check if the PPA exists and is not empty."
308 )
309 signing_key = response.json()
310 with NamedTemporaryFile("w+") as tmpfile:
311 tmpfile.write(signing_key)
312 tmpfile.flush()
313 with open(tmpfile.name) as keyfile:
314 gpg_cmd = [
315 "gpg",
316 "--ignore-time-conflict",
317 "--no-options",
318 "--no-keyring",
319 ]
320 with tempfile.NamedTemporaryFile(mode="wb+") as keyring:
321 subprocess.check_call(
322 gpg_cmd + ["--dearmor"], stdin=keyfile, stdout=keyring
323 )
324 os.fchmod(keyring.fileno(), 0o644)
325 instance.push_file(
326 destination=Path(
327 "/etc/apt/trusted.gpg.d"
328 f"/{owner}-{archive}-{distribution}.gpg"
329 ),
330 source=Path(keyring.name),
331 )
332
333
280def _install_apt_packages(334def _install_apt_packages(
281 job_name: str,335 job_name: str,
282 job: Job,336 job: Job,
@@ -313,6 +367,17 @@ def _install_apt_packages(
313 sources = template.render(**secrets)367 sources = template.render(**secrets)
314 sources += "\n"368 sources += "\n"
315369
370 ppas = set()
371 for package_repository in job.package_repositories:
372 if (
373 package_repository.type == PackageType.apt
374 and package_repository.ppa
375 ):
376 ppas.add(package_repository.ppa)
377
378 if ppas:
379 _import_signing_keys_for_ppas(instance, ppas)
380
316 with emit.open_stream("Replacing /etc/apt/sources.list") as stream:381 with emit.open_stream("Replacing /etc/apt/sources.list") as stream:
317 instance.push_file_io(382 instance.push_file_io(
318 destination=PurePath(sources_list_path),383 destination=PurePath(sources_list_path),
diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
index de8716a..df51380 100644
--- a/lpcraft/commands/tests/test_run.py
+++ b/lpcraft/commands/tests/test_run.py
@@ -16,6 +16,7 @@ from craft_providers.lxd import launch
16from fixtures import TempDir16from fixtures import TempDir
17from testtools.matchers import MatchesStructure17from testtools.matchers import MatchesStructure
1818
19from lpcraft.commands.run import LAUNCHPAD_API_BASE_URL
19from lpcraft.commands.tests import CommandBaseTestCase20from lpcraft.commands.tests import CommandBaseTestCase
20from lpcraft.errors import CommandError, ConfigurationError21from lpcraft.errors import CommandError, ConfigurationError
21from lpcraft.providers.tests import makeLXDProvider22from lpcraft.providers.tests import makeLXDProvider
@@ -484,6 +485,167 @@ class TestRun(RunBaseTestCase):
484 self.assertEqual("root", mock_info["group"])485 self.assertEqual("root", mock_info["group"])
485 self.assertEqual("root", mock_info["user"])486 self.assertEqual("root", mock_info["user"])
486487
488 @responses.activate
489 @patch("lpcraft.commands.run.get_provider")
490 @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
491 def test_importing_ppa_key_key_not_found(
492 self,
493 mock_get_host_architecture,
494 mock_get_provider,
495 ):
496 launcher = Mock(spec=launch)
497 provider = makeLXDProvider(lxd_launcher=launcher)
498 mock_get_provider.return_value = provider
499 execute_run = launcher.return_value.execute_run
500 execute_run.return_value = subprocess.CompletedProcess([], 0)
501
502 responses.get(
503 "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL),
504 match=[
505 responses.matchers.query_param_matcher(
506 {"ws.op": "getSigningKeyData"}
507 )
508 ],
509 status=404,
510 )
511 config = dedent(
512 """
513 pipeline:
514 - test
515 jobs:
516 test:
517 series: focal
518 architectures: amd64
519 run: ls -la
520 packages: [foo]
521 package-repositories:
522 - type: apt
523 ppa: example/foo
524 formats: [deb, deb-src]
525 suites: [focal]
526 """
527 )
528 Path(".launchpad.yaml").write_text(config)
529
530 result = self.run_command("run")
531 self.assertThat(
532 result,
533 MatchesStructure.byEquality(
534 exit_code=1,
535 errors=[
536 CommandError(
537 "Error retrieving the signing key for the"
538 " 'example/foo/ubuntu' ppa. Please check"
539 " if the PPA exists and is not empty."
540 )
541 ],
542 ),
543 )
544
545 @responses.activate
546 @patch("lpcraft.commands.run.get_provider")
547 @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
548 def test_importing_ppa_signing_key(
549 self,
550 mock_get_host_architecture,
551 mock_get_provider,
552 ):
553 launcher = Mock(spec=launch)
554 provider = makeLXDProvider(lxd_launcher=launcher)
555 mock_get_provider.return_value = provider
556 execute_run = launcher.return_value.execute_run
557 execute_run.return_value = subprocess.CompletedProcess([], 0)
558 test_key = dedent(
559 """
560 -----BEGIN PGP PUBLIC KEY BLOCK-----
561 Version: GnuPG v2
562
563 mI0ESUm55wEEALrxow0PCnGeCAebH9g5+wtZBfXZdx2vZts+XsTTHxDRsMNgMC9b
564 0klCgbydvkmF9WCphCjQ61Wp/Bh0C7DSXVCpA/xs55QB5VCUceIMZCbMTPq1h7Ht
565 cA1f+o6+OCPUntErG6eGize6kGhdjBNPOT+q4BSIL69rPuwfM9ZyAYcBABEBAAG0
566 JkxhdW5jaHBhZCBQUEEgZm9yIExhdW5jaHBhZCBEZXZlbG9wZXJziLYEEwECACAF
567 AklJuecCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAtH/tsClF0rxsQA/0Q
568 w0Yk+xIA1xibyf+UCF9/4fXzdo/tr76qxPRyFiv0uLbFOmW6t26jzpWBHocCHcCU
569 57l7rlcEzIHFMcS9Ol6MughP4lhywf9ceeqg2SD6AXjZ0iFarwkueTcHwff5j0lG
570 IzzCUVTYJ+m79f/r0dfctL2DwnX7JnT/41mEuR1qbokBHAQQAQIABgUCTB7s7wAK
571 CRDFXO8hUqH8T94pCACxl/Gdo82N01H82HvNBa8zQFixNQIwNJN/VxH3WfRvissW
572 OMTJnTnNOQErxUhqHrasvZf3djNoHeKRNToTTBaGiEwoySmEK05i4Toq74jWAOs6
573 flD2S8natWbobK5V+B2pXZl5g/4Ay21C3H1sZlUxDCcOH9Jh8/0feAZHoSQ/V1Xa
574 rEPb+TGdV0hP3Yp7+nIT91sYkj566kA8fjoxJrY/EvXGn98bhYMbMNbtS1Z0WeGp
575 zG2hiL6wLSLBxz4Ae9MShOMwNyC1zmr/d1wlF0Efx1N9HaRtRq2s/zqH+ebB7Sr+
576 V+SquObb0qr4eAjtslN5BxWROhf+wZM6WJO0Z6nBiQEcBBABAgAGBQJTHvsiAAoJ
577 EIngjfAzAr5Z8y4H/jltxz5OwHIDoiXsyWnpjO1SZUV6I6evKpSD7huYtd7MwFZC
578 0CgExsPPqLNQCUxITR+9jlqofi/QsTwP7Qq55VmIrKLrZ9KCK1qBnMa/YEXi6TeK
579 65lnyN6lNOdzhcsBm3s1/U9ewWp1vsw4UAclmu6tI8GUko+e32K1QjMtIjeVejQl
580 JCYDjuxfHhcFWyRo0TWu24F6VD3YxBHpne/M00yd2mLLpHdQrxw/vbvVhZkRDutQ
581 emKRA81ZM2WZ1iqYOXtEs5VrD/PtU0nvSAowgeWBmcOwWn3Om+pVsnSoFo46CDvo
582 C6YXOWMOMFIxfVhPWqlBkWQsnXFzgk/Xyo4vlTY==Wq6H
583 -----END PGP PUBLIC KEY BLOCK-----
584 """
585 )
586 test_key = json.dumps(test_key)
587 responses.get(
588 "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL),
589 match=[
590 responses.matchers.query_param_matcher(
591 {"ws.op": "getSigningKeyData"}
592 )
593 ],
594 body=test_key,
595 )
596 responses.get(
597 "{}/~example/+archive/debian/bar".format(LAUNCHPAD_API_BASE_URL),
598 match=[
599 responses.matchers.query_param_matcher(
600 {"ws.op": "getSigningKeyData"}
601 )
602 ],
603 body=test_key,
604 )
605 config = dedent(
606 """
607 pipeline:
608 - test
609 jobs:
610 test:
611 series: focal
612 architectures: amd64
613 run: ls -la
614 packages: [foo]
615 package-repositories:
616 - type: apt
617 ppa: example/foo
618 formats: [deb, deb-src]
619 suites: [focal]
620 - type: apt
621 ppa: example/debian/bar
622 formats: [deb, deb-src]
623 suites: [focal]
624 """
625 )
626 Path(".launchpad.yaml").write_text(config)
627
628 self.run_command("run")
629 mock_push_file = launcher.return_value.push_file
630 self.assertEqual(2, mock_push_file.call_count)
631 mock_push_file.assert_has_calls(
632 [
633 call(
634 destination=Path(
635 "/etc/apt/trusted.gpg.d/example-foo-ubuntu.gpg"
636 ),
637 source=ANY,
638 ),
639 call(
640 destination=Path(
641 "/etc/apt/trusted.gpg.d/example-bar-debian.gpg"
642 ),
643 source=ANY,
644 ),
645 ],
646 any_order=True,
647 )
648
487 @patch("lpcraft.commands.run.get_provider")649 @patch("lpcraft.commands.run.get_provider")
488 @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")650 @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
489 def test_updating_package_info_fails(651 def test_updating_package_info_fails(
diff --git a/tox.ini b/tox.ini
index 1d9be16..409c12f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -52,6 +52,7 @@ deps =
52 .[test]52 .[test]
53 mypy53 mypy
54 types-PyYAML54 types-PyYAML
55 types-requests
55commands =56commands =
56 mypy --cache-dir="{envdir}/mypy_cache" --strict {posargs:lpcraft}57 mypy --cache-dir="{envdir}/mypy_cache" --strict {posargs:lpcraft}
5758

Subscribers

People subscribed via source and target branches