Merge ppa-dev-tools:set-command-private-ppa into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: a116288136ae712e726d5e8f7830b64ca77830ba
Proposed branch: ppa-dev-tools:set-command-private-ppa
Merge into: ppa-dev-tools:main
Diff against target: 395 lines (+173/-31)
7 files modified
ppa/ppa.py (+24/-0)
ppa/ppa_group.py (+3/-1)
scripts/ppa (+43/-21)
tests/helpers.py (+5/-2)
tests/smoketest_ppa_set (+70/-0)
tests/test_ppa_group.py (+10/-0)
tests/test_scripts_ppa.py (+18/-7)
Reviewer Review Type Date Requested Status
Lena Voytek (community) Approve
Canonical Server packageset reviewers Pending
PpaDevTools Developers Pending
Canonical Server Reporter Pending
Review via email: mp+447032@code.launchpad.net

Description of the change

Implements support for creating private PPAs and toggling PPAs public vs. private.

Creating private PPAs requires special authorization that I don't seem to have, with the result that I've not been able to verify that this actually works correctly "live". However, I've included checks for some of the authorization errors I've hit, and implemented a slew of unit tests. I'll try to find someone with access to do the live verification, but apart from that I think this code is ready for review and landing.

As usual, to run the testsuite, do:

    $ make check

or

    $ pytest-3

And the smoketest has been updated to include toggling --private:

    $ ./tests/smoketest_ppa_set

However as mentioned, unless you have authorization to modify privacy on PPAs that will merely generate errors.

To post a comment you must log in.
Revision history for this message
Lena Voytek (lvoytek) wrote :

LGTM! just one note for one of the help strings

review: Approve
Revision history for this message
Bryce Harrington (bryce) wrote :

Thanks Lena, I've squashed that change in, updated this branch, and landed to main.

stirling: ~/src/PpaDevTools/ppa-dev-tools-commands-set$ git merge --ff-only set-command-private-ppa
Updating 4fa781a..0f7a075
Fast-forward
 ppa/ppa.py | 24 ++++++++++++++++++++++++
 ppa/ppa_group.py | 4 +++-
 scripts/ppa | 64 +++++++++++++++++++++++++++++++++++++++++++---------------------
 tests/helpers.py | 7 +++++--
 tests/smoketest_ppa_set | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_ppa_group.py | 10 ++++++++++
 tests/test_scripts_ppa.py | 25 ++++++++++++++++++-------
 7 files changed, 173 insertions(+), 31 deletions(-)
 create mode 100755 tests/smoketest_ppa_set
stirling: ~/src/PpaDevTools/ppa-dev-tools-commands-set$ git push
Enumerating objects: 57, done.
Counting objects: 100% (57/57), done.
Delta compression using up to 12 threads
Compressing objects: 100% (40/40), done.
Writing objects: 100% (47/47), 6.94 KiB | 6.94 MiB/s, done.
Total 47 (delta 29), reused 0 (delta 0), pack-reused 0
To git+ssh://git.launchpad.net/ppa-dev-tools
   4fa781a..0f7a075 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ppa/ppa.py b/ppa/ppa.py
2index 2dd46eb..c4ce1fe 100755
3--- a/ppa/ppa.py
4+++ b/ppa/ppa.py
5@@ -198,6 +198,30 @@ class Ppa:
6
7 @property
8 @lru_cache
9+ def is_private(self) -> bool:
10+ """Indicates if the PPA is private or public.
11+
12+ :rtype: bool
13+ :returns: True if the archive is private, False if public.
14+ """
15+ return self.archive.private
16+
17+ def set_private(self, private: bool):
18+ """Attempts to configure the PPA as private.
19+
20+ Note that PPAs can't be changed to private if they ever had any
21+ sources published, or if the owning person or team is not
22+ permitted to hold private PPAs.
23+
24+ :param bool private: Whether the PPA should be private or public.
25+ """
26+ if private is None:
27+ return
28+ self.archive.private = private
29+ self.archive.lp_save()
30+
31+ @property
32+ @lru_cache
33 def publish(self):
34 return self.archive.publish
35
36diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py
37index c9747a5..3061100 100755
38--- a/ppa/ppa_group.py
39+++ b/ppa/ppa_group.py
40@@ -88,13 +88,14 @@ class PpaGroup:
41 """
42 return self.service.people[self.name]
43
44- def create(self, ppa_name='ppa', ppa_description=None):
45+ def create(self, ppa_name='ppa', ppa_description=None, private=False):
46 """Registers a new PPA with Launchpad.
47
48 If a description is not provided a default one will be generated.
49
50 :param str ppa_name: Name for the PPA to create.
51 :param str ppa_description: Text description of the PPA.
52+ :param bool private: Limit access to PPA to only subscribed users and owner.
53 :rtype: Ppa
54 :returns: A Ppa object that describes the created PPA.
55
56@@ -103,6 +104,7 @@ class PpaGroup:
57 ppa_settings = {
58 'description': ppa_description,
59 'displayname': ppa_name,
60+ 'private': private
61 }
62
63 try:
64diff --git a/scripts/ppa b/scripts/ppa
65index 67dec11..ea624e0 100755
66--- a/scripts/ppa
67+++ b/scripts/ppa
68@@ -55,7 +55,7 @@ from inspect import currentframe
69 from textwrap import indent
70 from typing import Any
71 from distro_info import UbuntuDistroInfo
72-from lazr.restfulclient.errors import BadRequest
73+from lazr.restfulclient.errors import BadRequest, Unauthorized
74
75 try:
76 from ruamel import yaml
77@@ -193,33 +193,37 @@ def add_basic_config_options(parser: argparse.ArgumentParser) -> None:
78 help="A short description of the archive. URLs will be rendered as links. (See also 'ppa desc'.)"
79 )
80
81- # Enable/Disable uploading
82+ # Dependencies
83 parser.add_argument(
84- '--enable',
85- dest="set_enabled",
86- action='store_true',
87- help="Accept and build packages uploaded to the PPA."
88+ '--ppa-dependencies', '--ppa-depends',
89+ dest="ppa_dependencies", action='store',
90+ help="The set of other PPAs this PPA should use for satisfying build dependencies."
91 )
92+
93+ # Public/Private access
94 parser.add_argument(
95- '--disable',
96- dest="set_disabled",
97+ '--private', '-P',
98+ dest="private",
99 action='store_true',
100- help="Do not accept or build packages uploaded to the PPA."
101+ help=(
102+ "Restrict access to the PPA to its owner and subscribers. " +
103+ "This can only be changed if the archive has never had any " +
104+ "sources published and the owner/group has permission to do so."
105+ )
106 )
107-
108- # Dependencies
109 parser.add_argument(
110- '--ppa-dependencies', '--ppa-depends',
111- dest="ppa_dependencies", action='store',
112- help="The set of other PPAs this PPA should use for satisfying build dependencies."
113+ '--public',
114+ dest="private",
115+ action='store_false',
116+ help="Allow access to the PPA to anyone."
117 )
118
119+ # Publishing
120 parser.add_argument(
121 '--publish',
122 dest="publish", action='store_true',
123 help=("Allow built packages in the PPA to be made available for download.")
124 )
125-
126 parser.add_argument(
127 '--no-publish',
128 dest="publish", action='store_false',
129@@ -304,7 +308,7 @@ def create_arg_parser() -> argparse.ArgumentParser:
130 set_parser = subparser.add_parser(
131 'set',
132 argument_default=argparse.SUPPRESS,
133- help='set help',
134+ help='Applies one or more configuration changes to the PPA.',
135 prog=progname,
136 )
137 add_global_options(set_parser)
138@@ -429,7 +433,7 @@ def create_config(lp: Lp, args: argparse.Namespace) -> dict[str, Any]:
139 config = dict(DEFAULT_CONFIG)
140
141 # Map all non-empty elements from argparse Namespace into config dict
142- config.update({k: v for k, v in vars(args).items() if v})
143+ config.update({k: v for k, v in vars(args).items() if v is not None})
144
145 # Use defaults for any remaining parameters not yet configured
146 for k, v in DEFAULT_CONFIG.items():
147@@ -498,8 +502,6 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
148 warn("Could not determine owning person or team LP username")
149 return os.EX_USAGE
150
151- publish = config.get('publish', None)
152-
153 architectures = config.get('architectures', ARCHES_PPA_ALL)
154 if type(architectures) is str:
155 architectures = unpack_to_dict(architectures).keys()
156@@ -507,8 +509,12 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
157 try:
158 if not config.get('dry_run', False):
159 ppa_group = PpaGroup(service=lp, name=owner_name)
160- the_ppa = ppa_group.create(ppa_name, ppa_description=description)
161- the_ppa.set_publish(publish)
162+ the_ppa = ppa_group.create(
163+ ppa_name,
164+ ppa_description=description,
165+ private=config.get('private', False)
166+ )
167+ the_ppa.set_publish(config.get('publish', None))
168 if architectures:
169 the_ppa.set_architectures(architectures)
170 arch_str = ', '.join(the_ppa.architectures)
171@@ -534,6 +540,12 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
172 print(" sudo add-apt-repository -yus {}".format(the_ppa.address))
173 print(" sudo apt-get install <package(s)>")
174 return os.EX_OK
175+ except Unauthorized as e:
176+ error(f"Insufficient authorization to create '{ppa_name}' under ownership of '{owner_name}'.")
177+ return os.EX_NOPERM
178+ except KeyError as e:
179+ error(f"No such person or team '{owner_name}'")
180+ return os.EX_NOUSER
181 except PpaAlreadyExists as e:
182 warn(o2str(e.message))
183 return 3
184@@ -678,9 +690,18 @@ def command_set(lp: Lp, config: dict[str, str]) -> int:
185 if 'publish' in config:
186 the_ppa.archive.publish = config.get('publish')
187
188+ the_ppa.set_private(config.get('private', None))
189+
190 return the_ppa.archive.lp_save()
191+ except Unauthorized as e:
192+ if b'private' in e.content:
193+ error(f"Insufficient authorization to change privacy for PPA '{the_ppa.name}'.")
194+ else:
195+ error(f"Insufficient authorization to modify PPA '{the_ppa.name}'.")
196+ return os.EX_NOPERM
197 except PpaDoesNotExist as e:
198 print(e)
199+ return 1
200 except ValueError as e:
201 print(f"Error: {e}")
202 return os.EX_USAGE
203@@ -819,6 +840,7 @@ def command_wait(lp: Lp, config: dict[str, str]) -> int:
204 return os.EX_OK
205 except PpaDoesNotExist as e:
206 print(e)
207+ return 1
208 except ValueError as e:
209 print(f"Error: {e}")
210 return os.EX_USAGE
211diff --git a/tests/helpers.py b/tests/helpers.py
212index c607d3c..99d5e38 100644
213--- a/tests/helpers.py
214+++ b/tests/helpers.py
215@@ -44,8 +44,9 @@ class ArchiveMock:
216 self.displayname = name
217 self.description = description
218 self.owner = owner
219- self.publish = True
220+ self.private = False
221 self.processors = [ProcessorMock(proc_name) for proc_name in ARCHES_PPA_DEFAULT]
222+ self.publish = True
223 self.published_sources = []
224
225 def setProcessors(self, processors):
226@@ -64,12 +65,14 @@ class PersonMock:
227 self.name = name
228 self._ppas = []
229
230- def createPPA(self, name, description, displayname):
231+ def createPPA(self, name, description, displayname, private=None):
232 for ppa in self._ppas:
233 if ppa.name == name:
234 raise PpaAlreadyExists(name)
235 ppa = Ppa(name, self.name, description)
236 Ppa.archive = ArchiveMock(ppa.name, ppa.description, self)
237+ if isinstance(private, bool):
238+ Ppa.archive.private = private
239 self._ppas.append(ppa)
240 return True
241
242diff --git a/tests/smoketest_ppa_set b/tests/smoketest_ppa_set
243new file mode 100755
244index 0000000..7724d27
245--- /dev/null
246+++ b/tests/smoketest_ppa_set
247@@ -0,0 +1,70 @@
248+#!/bin/bash
249+
250+testsdir=$(dirname "$(readlink -f "${0}")")
251+progdir="${testsdir}/../scripts"
252+script="${progdir}/ppa"
253+ppa_name="ppa-test-set-${RANDOM}"
254+
255+[ -e "${script}" ] || {
256+ echo "Could not locate ppa command '${script}'."
257+ exit 1
258+}
259+
260+ppa_create() {
261+ echo -n "* Creating ${ppa_name} .."
262+ if "${script}" create "${ppa_name}"; then
263+ echo ". OK"
264+ else
265+ echo "! ERROR"
266+ exit 1
267+ fi
268+}
269+
270+ppa_destroy() {
271+ echo -n "* Destroying ${ppa_name} .."
272+ if "${script}" destroy "${ppa_name}"; then
273+ echo ". OK"
274+ else
275+ echo "! ERROR"
276+ exit 1
277+ fi
278+}
279+
280+ppa_status() {
281+ "${script}" status "${ppa_name}"
282+}
283+
284+ppa_set() {
285+ echo -n "* Setting ${1} .."
286+ if "${script}" set "${ppa_name}" "${@}"; then
287+ echo ". OK"
288+ else
289+ echo "! ERROR"
290+ fi
291+}
292+
293+echo "### Default configuration ###"
294+ppa_create && \
295+ trap "ppa_destroy" EXIT
296+ppa_status
297+
298+echo
299+echo "### Set parameters ###"
300+ppa_set --displayname "parameters-set"
301+ppa_set --description "Updated PPA with various settings changed"
302+ppa_set --all-architectures
303+ppa_set --no-publish
304+ppa_status
305+
306+echo
307+echo "### Restore to default parameters ###"
308+ppa_set --displayname "${ppa_name}"
309+ppa_set --description ""
310+ppa_set --default-architectures
311+ppa_set --publish
312+ppa_status
313+
314+echo
315+echo "### Private PPA ###"
316+ppa_set --private
317+ppa_set --public
318diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py
319index f53799f..6539c3a 100644
320--- a/tests/test_ppa_group.py
321+++ b/tests/test_ppa_group.py
322@@ -69,6 +69,16 @@ def test_create_with_owner():
323 assert ppa.address == 'ppa:test_owner_name/ppa_test_name'
324
325
326+def test_create_private():
327+ """Check creating a private PPA."""
328+ lp = LpServiceMock()
329+ ppa_group = PpaGroup(service=lp, name='me')
330+ ppa = ppa_group.create('private_ppa', private=True)
331+ assert ppa is not None
332+ assert ppa.address == 'ppa:me/private_ppa'
333+ assert ppa.is_private is True
334+
335+
336 def test_list_ppas():
337 """Check listing the PPAs for a PPA group."""
338 test_ppa_list = ['a', 'b', 'c', 'd']
339diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py
340index f82a9e4..93988d0 100644
341--- a/tests/test_scripts_ppa.py
342+++ b/tests/test_scripts_ppa.py
343@@ -232,13 +232,16 @@ def test_create_arg_parser_basic_config(command):
344 assert args.description == 'x'
345 args.description = None
346
347- # Check --enable / --disable
348- args = parser.parse_args([command, 'test-ppa', '--enable'])
349- assert args.set_enabled is True
350- args.set_enabled = False
351- args = parser.parse_args([command, 'test-ppa', '--disable'])
352- assert args.set_disabled is True
353- args.set_disabled = False
354+ # Check --public / -P|--private
355+ args = parser.parse_args([command, 'test-ppa', '--public'])
356+ assert args.private is False
357+ args.public = None
358+ args = parser.parse_args([command, 'test-ppa', '--private'])
359+ assert args.private is True
360+ args.private = None
361+ args = parser.parse_args([command, 'test-ppa', '-P'])
362+ assert args.private is True
363+ args.private = None
364
365 # Check --ppa-dependencies <PPA[,...]>
366 args = parser.parse_args([command, 'test-ppa', '--ppa-dependencies', 'a,b,c'])
367@@ -431,6 +434,8 @@ def test_create_arg_parser_tests():
368 (['create', 'aa/bb'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
369 (['create', 'bb', '--owner', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
370 (['create', 'bb', '--owner-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
371+ (['create', 'bb', '--private'], {'command': 'create', 'private': True}),
372+ (['create', 'bb', '--public'], {'command': 'create', 'private': False}),
373 (['create', 'bb', '--team', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
374 (['create', 'bb', '--team-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
375 ])
376@@ -489,6 +494,8 @@ def test_create_config_from_args_error(args, expected_exception):
377 (None, {'ppa_name': 'a'}, {'displayname': 'a'}),
378 (None, {'publish': True}, {'publish': True}),
379 (None, {'publish': False}, {'publish': False}),
380+ (None, {'private': True}, {'private': True}),
381+ (None, {'private': False}, {'private': False}),
382 ])
383 def test_command_create(fake_config, monkeypatch, stdin, params, expected_ppa_config):
384 '''Checks create command produces a PPA with expected configuration.'''
385@@ -603,6 +610,10 @@ def test_command_not_exists(fake_config):
386 ({}, {'publish': True}),
387 ({'publish': False}, {'publish': False}),
388 ({'publish': True}, {'publish': True}),
389+
390+ ({}, {'private': False}),
391+ ({'private': False}, {'private': False}),
392+ ({'private': True}, {'private': True}),
393 ])
394 def test_command_set(fake_config, params, expected_ppa_config):
395 '''Checks that the set command properly requests PPA configuration changes.'''

Subscribers

People subscribed via source and target branches

to all changes: