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
1diff --git a/NEWS.rst b/NEWS.rst
2index 05b605f..fb9c5a2 100644
3--- a/NEWS.rst
4+++ b/NEWS.rst
5@@ -8,6 +8,9 @@ Version history
6 - Allow specifying PPAs using the shortform notation,
7 e.g. `ppa:launchpad/ubuntu/ppa`.
8
9+- Automatically import the signing keys for PPAs specified using
10+ the short-form notation.
11+
12 0.0.37 (2022-12-09)
13 ===================
14
15diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
16index d91bd41..72bc3a4 100644
17--- a/lpcraft/commands/run.py
18+++ b/lpcraft/commands/run.py
19@@ -7,11 +7,14 @@ import itertools
20 import json
21 import os
22 import shlex
23+import subprocess
24+import tempfile
25 from argparse import ArgumentParser, Namespace
26 from pathlib import Path, PurePath
27 from tempfile import NamedTemporaryFile
28 from typing import Dict, List, Optional, Set
29
30+import requests
31 import yaml
32 from craft_cli import BaseCommand, EmitterMode, emit
33 from craft_providers import Executor, lxd
34@@ -21,13 +24,23 @@ from jinja2 import BaseLoader, Environment
35 from pluggy import PluginManager
36
37 from lpcraft import env
38-from lpcraft.config import Config, Input, Job, Output
39+from lpcraft.config import (
40+ Config,
41+ Input,
42+ Job,
43+ Output,
44+ PackageType,
45+ PPAShortFormURL,
46+ get_ppa_url_parts,
47+)
48 from lpcraft.errors import CommandError
49 from lpcraft.plugin.manager import get_plugin_manager
50 from lpcraft.plugins import PLUGINS
51 from lpcraft.providers import Provider, get_provider
52 from lpcraft.utils import get_host_architecture
53
54+LAUNCHPAD_API_BASE_URL = "https://api.launchpad.net/devel"
55+
56
57 def _check_relative_path(path: PurePath, container: PurePath) -> PurePath:
58 """Check that `path` does not escape `container`.
59@@ -277,6 +290,47 @@ def _resolve_runtime_value(
60 return command_value
61
62
63+def _import_signing_keys_for_ppas(
64+ instance: lxd.LXDInstance, ppas: Set[PPAShortFormURL]
65+) -> None:
66+ for ppa in ppas:
67+ owner, distribution, archive = get_ppa_url_parts(ppa)
68+ signing_key_url = (
69+ f"{LAUNCHPAD_API_BASE_URL}/~{owner}/+archive/{distribution}"
70+ f"/{archive}?ws.op=getSigningKeyData"
71+ )
72+ response = requests.get(signing_key_url)
73+ if not response.ok:
74+ raise CommandError(
75+ "Error retrieving the signing key for the"
76+ f" '{owner}/{archive}/{distribution}' ppa."
77+ " Please check if the PPA exists and is not empty."
78+ )
79+ signing_key = response.json()
80+ with NamedTemporaryFile("w+") as tmpfile:
81+ tmpfile.write(signing_key)
82+ tmpfile.flush()
83+ with open(tmpfile.name) as keyfile:
84+ gpg_cmd = [
85+ "gpg",
86+ "--ignore-time-conflict",
87+ "--no-options",
88+ "--no-keyring",
89+ ]
90+ with tempfile.NamedTemporaryFile(mode="wb+") as keyring:
91+ subprocess.check_call(
92+ gpg_cmd + ["--dearmor"], stdin=keyfile, stdout=keyring
93+ )
94+ os.fchmod(keyring.fileno(), 0o644)
95+ instance.push_file(
96+ destination=Path(
97+ "/etc/apt/trusted.gpg.d"
98+ f"/{owner}-{archive}-{distribution}.gpg"
99+ ),
100+ source=Path(keyring.name),
101+ )
102+
103+
104 def _install_apt_packages(
105 job_name: str,
106 job: Job,
107@@ -313,6 +367,17 @@ def _install_apt_packages(
108 sources = template.render(**secrets)
109 sources += "\n"
110
111+ ppas = set()
112+ for package_repository in job.package_repositories:
113+ if (
114+ package_repository.type == PackageType.apt
115+ and package_repository.ppa
116+ ):
117+ ppas.add(package_repository.ppa)
118+
119+ if ppas:
120+ _import_signing_keys_for_ppas(instance, ppas)
121+
122 with emit.open_stream("Replacing /etc/apt/sources.list") as stream:
123 instance.push_file_io(
124 destination=PurePath(sources_list_path),
125diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
126index 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 from fixtures import TempDir
131 from testtools.matchers import MatchesStructure
132
133+from lpcraft.commands.run import LAUNCHPAD_API_BASE_URL
134 from lpcraft.commands.tests import CommandBaseTestCase
135 from lpcraft.errors import CommandError, ConfigurationError
136 from lpcraft.providers.tests import makeLXDProvider
137@@ -484,6 +485,167 @@ class TestRun(RunBaseTestCase):
138 self.assertEqual("root", mock_info["group"])
139 self.assertEqual("root", mock_info["user"])
140
141+ @responses.activate
142+ @patch("lpcraft.commands.run.get_provider")
143+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
144+ def test_importing_ppa_key_key_not_found(
145+ self,
146+ mock_get_host_architecture,
147+ mock_get_provider,
148+ ):
149+ launcher = Mock(spec=launch)
150+ provider = makeLXDProvider(lxd_launcher=launcher)
151+ mock_get_provider.return_value = provider
152+ execute_run = launcher.return_value.execute_run
153+ execute_run.return_value = subprocess.CompletedProcess([], 0)
154+
155+ responses.get(
156+ "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL),
157+ match=[
158+ responses.matchers.query_param_matcher(
159+ {"ws.op": "getSigningKeyData"}
160+ )
161+ ],
162+ status=404,
163+ )
164+ config = dedent(
165+ """
166+ pipeline:
167+ - test
168+ jobs:
169+ test:
170+ series: focal
171+ architectures: amd64
172+ run: ls -la
173+ packages: [foo]
174+ package-repositories:
175+ - type: apt
176+ ppa: example/foo
177+ formats: [deb, deb-src]
178+ suites: [focal]
179+ """
180+ )
181+ Path(".launchpad.yaml").write_text(config)
182+
183+ result = self.run_command("run")
184+ self.assertThat(
185+ result,
186+ MatchesStructure.byEquality(
187+ exit_code=1,
188+ errors=[
189+ CommandError(
190+ "Error retrieving the signing key for the"
191+ " 'example/foo/ubuntu' ppa. Please check"
192+ " if the PPA exists and is not empty."
193+ )
194+ ],
195+ ),
196+ )
197+
198+ @responses.activate
199+ @patch("lpcraft.commands.run.get_provider")
200+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
201+ def test_importing_ppa_signing_key(
202+ self,
203+ mock_get_host_architecture,
204+ mock_get_provider,
205+ ):
206+ launcher = Mock(spec=launch)
207+ provider = makeLXDProvider(lxd_launcher=launcher)
208+ mock_get_provider.return_value = provider
209+ execute_run = launcher.return_value.execute_run
210+ execute_run.return_value = subprocess.CompletedProcess([], 0)
211+ test_key = dedent(
212+ """
213+ -----BEGIN PGP PUBLIC KEY BLOCK-----
214+ Version: GnuPG v2
215+
216+ mI0ESUm55wEEALrxow0PCnGeCAebH9g5+wtZBfXZdx2vZts+XsTTHxDRsMNgMC9b
217+ 0klCgbydvkmF9WCphCjQ61Wp/Bh0C7DSXVCpA/xs55QB5VCUceIMZCbMTPq1h7Ht
218+ cA1f+o6+OCPUntErG6eGize6kGhdjBNPOT+q4BSIL69rPuwfM9ZyAYcBABEBAAG0
219+ JkxhdW5jaHBhZCBQUEEgZm9yIExhdW5jaHBhZCBEZXZlbG9wZXJziLYEEwECACAF
220+ AklJuecCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAtH/tsClF0rxsQA/0Q
221+ w0Yk+xIA1xibyf+UCF9/4fXzdo/tr76qxPRyFiv0uLbFOmW6t26jzpWBHocCHcCU
222+ 57l7rlcEzIHFMcS9Ol6MughP4lhywf9ceeqg2SD6AXjZ0iFarwkueTcHwff5j0lG
223+ IzzCUVTYJ+m79f/r0dfctL2DwnX7JnT/41mEuR1qbokBHAQQAQIABgUCTB7s7wAK
224+ CRDFXO8hUqH8T94pCACxl/Gdo82N01H82HvNBa8zQFixNQIwNJN/VxH3WfRvissW
225+ OMTJnTnNOQErxUhqHrasvZf3djNoHeKRNToTTBaGiEwoySmEK05i4Toq74jWAOs6
226+ flD2S8natWbobK5V+B2pXZl5g/4Ay21C3H1sZlUxDCcOH9Jh8/0feAZHoSQ/V1Xa
227+ rEPb+TGdV0hP3Yp7+nIT91sYkj566kA8fjoxJrY/EvXGn98bhYMbMNbtS1Z0WeGp
228+ zG2hiL6wLSLBxz4Ae9MShOMwNyC1zmr/d1wlF0Efx1N9HaRtRq2s/zqH+ebB7Sr+
229+ V+SquObb0qr4eAjtslN5BxWROhf+wZM6WJO0Z6nBiQEcBBABAgAGBQJTHvsiAAoJ
230+ EIngjfAzAr5Z8y4H/jltxz5OwHIDoiXsyWnpjO1SZUV6I6evKpSD7huYtd7MwFZC
231+ 0CgExsPPqLNQCUxITR+9jlqofi/QsTwP7Qq55VmIrKLrZ9KCK1qBnMa/YEXi6TeK
232+ 65lnyN6lNOdzhcsBm3s1/U9ewWp1vsw4UAclmu6tI8GUko+e32K1QjMtIjeVejQl
233+ JCYDjuxfHhcFWyRo0TWu24F6VD3YxBHpne/M00yd2mLLpHdQrxw/vbvVhZkRDutQ
234+ emKRA81ZM2WZ1iqYOXtEs5VrD/PtU0nvSAowgeWBmcOwWn3Om+pVsnSoFo46CDvo
235+ C6YXOWMOMFIxfVhPWqlBkWQsnXFzgk/Xyo4vlTY==Wq6H
236+ -----END PGP PUBLIC KEY BLOCK-----
237+ """
238+ )
239+ test_key = json.dumps(test_key)
240+ responses.get(
241+ "{}/~example/+archive/ubuntu/foo".format(LAUNCHPAD_API_BASE_URL),
242+ match=[
243+ responses.matchers.query_param_matcher(
244+ {"ws.op": "getSigningKeyData"}
245+ )
246+ ],
247+ body=test_key,
248+ )
249+ responses.get(
250+ "{}/~example/+archive/debian/bar".format(LAUNCHPAD_API_BASE_URL),
251+ match=[
252+ responses.matchers.query_param_matcher(
253+ {"ws.op": "getSigningKeyData"}
254+ )
255+ ],
256+ body=test_key,
257+ )
258+ config = dedent(
259+ """
260+ pipeline:
261+ - test
262+ jobs:
263+ test:
264+ series: focal
265+ architectures: amd64
266+ run: ls -la
267+ packages: [foo]
268+ package-repositories:
269+ - type: apt
270+ ppa: example/foo
271+ formats: [deb, deb-src]
272+ suites: [focal]
273+ - type: apt
274+ ppa: example/debian/bar
275+ formats: [deb, deb-src]
276+ suites: [focal]
277+ """
278+ )
279+ Path(".launchpad.yaml").write_text(config)
280+
281+ self.run_command("run")
282+ mock_push_file = launcher.return_value.push_file
283+ self.assertEqual(2, mock_push_file.call_count)
284+ mock_push_file.assert_has_calls(
285+ [
286+ call(
287+ destination=Path(
288+ "/etc/apt/trusted.gpg.d/example-foo-ubuntu.gpg"
289+ ),
290+ source=ANY,
291+ ),
292+ call(
293+ destination=Path(
294+ "/etc/apt/trusted.gpg.d/example-bar-debian.gpg"
295+ ),
296+ source=ANY,
297+ ),
298+ ],
299+ any_order=True,
300+ )
301+
302 @patch("lpcraft.commands.run.get_provider")
303 @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
304 def test_updating_package_info_fails(
305diff --git a/tox.ini b/tox.ini
306index 1d9be16..409c12f 100644
307--- a/tox.ini
308+++ b/tox.ini
309@@ -52,6 +52,7 @@ deps =
310 .[test]
311 mypy
312 types-PyYAML
313+ types-requests
314 commands =
315 mypy --cache-dir="{envdir}/mypy_cache" --strict {posargs:lpcraft}
316

Subscribers

People subscribed via source and target branches