Merge ~ballot/jenkins-plugin-manager/+git/jenkins-plugin-manager:master into jenkins-plugin-manager:master
- Git
- lp:~ballot/jenkins-plugin-manager/+git/jenkins-plugin-manager
- master
- Merge into master
Status: | Merged |
---|---|
Approved by: | Tom Haddon |
Approved revision: | d3e15d25fe82715581c2497d25db4e5d76c8223f |
Merged at revision: | 421dc1082891d2215f1198de9eef377b82fe458d |
Proposed branch: | ~ballot/jenkins-plugin-manager/+git/jenkins-plugin-manager:master |
Merge into: | jenkins-plugin-manager:master |
Diff against target: |
1059 lines (+946/-0) 19 files modified
.flake8 (+4/-0) .gitignore (+9/-0) Makefile (+67/-0) README.md (+48/-0) debian/changelog (+19/-0) debian/compat (+1/-0) debian/control (+14/-0) debian/rules (+22/-0) debian/source/format (+1/-0) jenkins_plugin_manager/__init__.py (+0/-0) jenkins_plugin_manager/core.py (+94/-0) jenkins_plugin_manager/exceptions.py (+19/-0) jenkins_plugin_manager/main.py (+122/-0) jenkins_plugin_manager/plugin.py (+216/-0) requirements.txt (+2/-0) setup.py (+44/-0) tests/test_jenkins_plugin_manager_core.py (+52/-0) tests/test_jenkins_plugin_manager_plugin.py (+176/-0) tox.ini (+36/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Haddon | Approve | ||
Stuart Bishop (community) | Approve | ||
Review via email: mp+375262@code.launchpad.net |
Commit message
* Initial commit
* Change name into jenkins_
Description of the change
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Benjamin Allot (ballot) wrote : | # |
The JSON files can be ignored because they are a snapshot of https:/
TODO in others MP:
* Download jenkins package from cli (only the library is implmented for now)
* Checksum of the downloaded package
* add various option in the "download" subcommand to allow optional dependencies and without if needed
* symlink the latest downloaded version if desired
* document the format of the plugin --from-file format
Tom Haddon (mthaddon) wrote : | # |
Some comments inline
Stuart Bishop (stub) wrote : | # |
All looks great and in a good style. Some inline comments in addition to Toms. Giving this a preemptive approve, although maybe the branch is being handed over instead of landed per discussion with Tom.
Benjamin Allot (ballot) wrote : | # |
Inline comments of both addressed.
Basically took all of them into account except the try/import mock , because this would be needed for a python 3.2 support (the one on Precise). I added a comment related to that.
Also added a few tests on the JenkinsCore class.
Tom Haddon (mthaddon) wrote : | # |
No need for precise/python3.2 support, and looks like the url property of setup.py needs updating.
Tom Haddon (mthaddon) wrote : | # |
Looks good, thanks.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 421dc1082891d22
Preview Diff
1 | diff --git a/.flake8 b/.flake8 |
2 | new file mode 100644 |
3 | index 0000000..9a6d4d5 |
4 | --- /dev/null |
5 | +++ b/.flake8 |
6 | @@ -0,0 +1,4 @@ |
7 | +[flake8] |
8 | +exclude = .venv |
9 | +max-complexity = 10 |
10 | +max-line-length = 120 |
11 | diff --git a/.gitignore b/.gitignore |
12 | new file mode 100644 |
13 | index 0000000..e238002 |
14 | --- /dev/null |
15 | +++ b/.gitignore |
16 | @@ -0,0 +1,9 @@ |
17 | +*.pyc |
18 | +*.swp |
19 | +*~ |
20 | +.venv |
21 | +/.coverage |
22 | +/.pybuild |
23 | +/.tox |
24 | +__pycache__ |
25 | +debian/files |
26 | diff --git a/Makefile b/Makefile |
27 | new file mode 100644 |
28 | index 0000000..67498c0 |
29 | --- /dev/null |
30 | +++ b/Makefile |
31 | @@ -0,0 +1,67 @@ |
32 | +PROJECTPATH = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) |
33 | + |
34 | +VENV := .venv |
35 | +VENV_PIP := $(PROJECTPATH)/$(VENV)/bin/pip |
36 | +VENV_PYTHON := $(PROJECTPATH)/$(VENV)/bin/python3 |
37 | + |
38 | +BLACK := $(VENV_PYTHON) -m black |
39 | +FLAKE8 := $(VENV_PYTHON) -m flake8 |
40 | + |
41 | +PROJECT := jenkins-plugin-manager |
42 | +PKG_NAME := python3-$(PROJECT) |
43 | + |
44 | +all: lint test |
45 | + |
46 | +clean: clean-python clean-venv clean-tox |
47 | + |
48 | +clean-python: |
49 | + @find $(PROJECTPATH) -name "__pycache__" -type d -exec rm -rf {} + |
50 | + @find $(PROJECTPATH) -name "pytest_cache" -type d -exec rm -rf {} + |
51 | + |
52 | +clean-tox: |
53 | + rm -rf $(PROJECTPATH)/.tox |
54 | + |
55 | +clean-venv: |
56 | + rm -rf $(PROJECTPATH)/$(VENV) |
57 | + |
58 | +dch: |
59 | + gbp dch --debian-tag='%(version)s' -D bionic --git-log --first-parent |
60 | + |
61 | +deb-src: |
62 | + debuild -S -sa -I.git -I.tox -I$(VENV) |
63 | + |
64 | +deb: |
65 | + debuild -us -uc -sa -I.git -I.tox -I$(VENV) |
66 | + |
67 | +/var/cache/pbuilder/base-bionic-amd64.cow: |
68 | + ARCH=amd64 DIST=bionic git-pbuilder create |
69 | + |
70 | +pbuilder-deb-src: /var/cache/pbuilder/base-bionic-amd64.cow |
71 | + GIT_PBUILDER_OPTIONS='--source-only-changes' \ |
72 | + ARCH=amd64 DIST=bionic git-pbuilder -sa -I.git -I.tox -I$(VENV) |
73 | + @echo "Now use debsign on $(PROJECTPATH)../$(PKG_NAME)_x.y_source.changes" |
74 | + |
75 | +install-build-depends: |
76 | + sudo apt install \ |
77 | + debhelper \ |
78 | + git-buildpackage \ |
79 | + virtualenv |
80 | + |
81 | +lint: lint-python |
82 | + |
83 | +# See .flake8 for config options. |
84 | +lint-python: |
85 | + tox -e black |
86 | + tox -e lint |
87 | + |
88 | +test: test-python |
89 | + |
90 | +test-python: |
91 | + $(VENV_PYTHON) -m unittest discover -v tests |
92 | + |
93 | +$(VENV): |
94 | + virtualenv -p python3 $(PROJECTPATH)/$(VENV) |
95 | + $(VENV_PIP) install -I -r requirements.txt |
96 | + |
97 | +.PHONY: test lint lint-python test-python all lint lint-python clean clean-python clean-tox clean-venv deb-src dch |
98 | + |
99 | diff --git a/README.md b/README.md |
100 | new file mode 100644 |
101 | index 0000000..fd3882f |
102 | --- /dev/null |
103 | +++ b/README.md |
104 | @@ -0,0 +1,48 @@ |
105 | +# Jenkins Plugin Manager |
106 | + |
107 | +This repository contains the jenkins\_plugin\_manager python module. |
108 | +this module allows a management of the Jenkins plugins and their dependencies as well as getting them |
109 | + |
110 | +The Launchpad project can be found |
111 | +[here](https://launchpad.net/jenkins-plugin-manager). |
112 | + |
113 | +The code for this repository can be cloned with: |
114 | + |
115 | + git clone git+ssh://git.launchpad.net/jenkins-plugin-manager |
116 | + |
117 | +## Code Location |
118 | + |
119 | +The Python code can be found at `./jenkins_plugin_manager.py`, with tests in the |
120 | +`tests` directory. Lint configuration can be found in `./.flake8`. |
121 | + |
122 | +## Testing Code |
123 | + |
124 | +To get started, you need `make` and `tox`. |
125 | + |
126 | + make install-build-depends |
127 | + make test |
128 | + |
129 | +## Building a Release |
130 | + |
131 | +The release process for a project would normally be something like: |
132 | + |
133 | + [edit setup.py with the right version number (if applicable)] |
134 | + make dch |
135 | + [check and edit debian/changelog] |
136 | + git commit -m 'x.y.z release' debian/changelog |
137 | + export GPG\_TTY=$(tty) |
138 | + git tag -s -u <keyid> -m 'x.y.z release' x.y.z |
139 | + git push --tags |
140 | + make deb-src |
141 | + dput ppa:jenkins-plugin-manager-devs/jpm-devel ../python3-jenkins-plugin-manager\_x.y.z\_source.changes |
142 | + |
143 | +Where `x.y.z` is the appropriate version number. |
144 | +This will upload the debian source package to the |
145 | +[devel PPA](https://launchpad.net/~jenkins-plugin-manager-devs/+archive/ubuntu/jpm-devel) |
146 | +for building. |
147 | + |
148 | +Once the new packages have been successfully tested, they can be |
149 | +[copied](https://launchpad.net/~jenkins-plugin-manager-devs/+archive/ubuntu/jpm-devel/+copy-packages) |
150 | +as binary packages (not rebuilt from source) to the |
151 | +[production PPA](https://launchpad.net/~jenkins-plugin-manager-devs/+archive/ubuntu/jpm) |
152 | +The production environment can then be updated from this PPA. |
153 | diff --git a/debian/changelog b/debian/changelog |
154 | new file mode 100644 |
155 | index 0000000..5a48582 |
156 | --- /dev/null |
157 | +++ b/debian/changelog |
158 | @@ -0,0 +1,19 @@ |
159 | +python3-jenkins-plugin-manager (0.3) bionic; urgency=medium |
160 | + |
161 | + * Address inline comments |
162 | + * 0.3 release |
163 | + |
164 | + -- Benjamin Allot (ballot@Canonical) <benjamin.allot@canonical.com> Mon, 11 Nov 2019 23:12:30 +0100 |
165 | + |
166 | +python3-jenkins-plugin-manager (0.2ubuntu1) bionic; urgency=medium |
167 | + |
168 | + * 0.2ubuntu1 release |
169 | + |
170 | + -- Benjamin Allot (ballot@Canonical) <benjamin.allot@canonical.com> Fri, 08 Nov 2019 10:02:45 +0100 |
171 | + |
172 | +python3-jenkins-plugin-manager (0.1ubuntu1) bionic; urgency=medium |
173 | + |
174 | + * Initial commit |
175 | + * Change name into jenkins_plugin_manager and add debian package |
176 | + |
177 | + -- Benjamin Allot (ballot@Canonical) <benjamin.allot@canonical.com> Fri, 08 Nov 2019 03:49:08 +0100 |
178 | diff --git a/debian/compat b/debian/compat |
179 | new file mode 100644 |
180 | index 0000000..f599e28 |
181 | --- /dev/null |
182 | +++ b/debian/compat |
183 | @@ -0,0 +1 @@ |
184 | +10 |
185 | diff --git a/debian/control b/debian/control |
186 | new file mode 100644 |
187 | index 0000000..0b7ec32 |
188 | --- /dev/null |
189 | +++ b/debian/control |
190 | @@ -0,0 +1,14 @@ |
191 | +Source: python3-jenkins-plugin-manager |
192 | +Section: misc |
193 | +Priority: optional |
194 | +Maintainer: Benjamin Allot <benjamin.allot@canonical.com> |
195 | +Build-Depends: debhelper (>= 11), dh-python, python3-all, python3-setuptools, virtualenv |
196 | +Standards-Version: 3.9.7 |
197 | +X-Python3-Version: >= 3.2 |
198 | + |
199 | +Package: python3-jenkins-plugin-manager |
200 | +Architecture: any |
201 | +Depends: ${misc:Depends}, ${python3:Depends} |
202 | +Description: Jenkins Plugin Manager |
203 | + A python module used for managing Jenkins plugins dependencies |
204 | + and downloading them. |
205 | diff --git a/debian/rules b/debian/rules |
206 | new file mode 100755 |
207 | index 0000000..b867466 |
208 | --- /dev/null |
209 | +++ b/debian/rules |
210 | @@ -0,0 +1,22 @@ |
211 | +#!/usr/bin/make -f |
212 | + |
213 | +# Determine the current release |
214 | +# # Use specific control and changelog files for Precise |
215 | +# |
216 | +codename ?= $(shell lsb_release -cs) |
217 | +module := jenkins_plugin_manager |
218 | +pkg_name := python3-$(module) |
219 | + |
220 | +export DH_VERBOSE=1 |
221 | +export PYBUILD_NAME=$(module) |
222 | + |
223 | +%: |
224 | + dh $@ --with python3 --buildsystem=pybuild |
225 | + |
226 | +override_dh_auto_test: |
227 | + |
228 | +override_dh_auto_clean: |
229 | + dh_auto_clean |
230 | + rm -rf build |
231 | + rm -rf *.egg-info |
232 | + |
233 | diff --git a/debian/source/format b/debian/source/format |
234 | new file mode 100644 |
235 | index 0000000..d3827e7 |
236 | --- /dev/null |
237 | +++ b/debian/source/format |
238 | @@ -0,0 +1 @@ |
239 | +1.0 |
240 | diff --git a/jenkins_plugin_manager/__init__.py b/jenkins_plugin_manager/__init__.py |
241 | new file mode 100644 |
242 | index 0000000..e69de29 |
243 | --- /dev/null |
244 | +++ b/jenkins_plugin_manager/__init__.py |
245 | diff --git a/jenkins_plugin_manager/core.py b/jenkins_plugin_manager/core.py |
246 | new file mode 100644 |
247 | index 0000000..8b25ad0 |
248 | --- /dev/null |
249 | +++ b/jenkins_plugin_manager/core.py |
250 | @@ -0,0 +1,94 @@ |
251 | +""" |
252 | +Jenkins Core module. |
253 | + |
254 | +This module loads a file like "https://updates.jenkins.io/stable-2.190/latestCore.txt" |
255 | +to determine the latest "stable" or "current" Jenkins version. |
256 | +It then provides methods through a class to retrieve the latest jenkins package |
257 | +on "https://pkg.jenkins.io/". |
258 | +""" |
259 | + |
260 | +import logging |
261 | +import os |
262 | +import urllib.request |
263 | + |
264 | +logger = logging.getLogger(__name__) |
265 | + |
266 | + |
267 | +DEFAULT_JENKINS_CORE_URL = "https://updates.jenkins.io" |
268 | +DEFAULT_JENKINS_REPO = "https://pkg.jenkins.io" |
269 | +DEFAULT_JENKINS_RELEASE = "stable" |
270 | + |
271 | + |
272 | +class JenkinsCore(object): |
273 | + """Represents Jenkins core manager.""" |
274 | + |
275 | + def __init__( |
276 | + self, |
277 | + jenkins_core_url=DEFAULT_JENKINS_CORE_URL, |
278 | + jenkins_repo=DEFAULT_JENKINS_REPO, |
279 | + jenkins_release=DEFAULT_JENKINS_RELEASE, |
280 | + ): |
281 | + """Instantiate the JenkinsCore class. |
282 | + |
283 | + Load the latest version of the `jenkins_release` available. |
284 | + |
285 | + :param str jenkins_core_url: Url where the latest Jenkins core version is set. |
286 | + :param str jenkins_repo: Url where the Jenkins binary packages are stored. |
287 | + :param str jenkins_release: Flavor of Jenkins. Values are "stable" or "current". |
288 | + """ |
289 | + # TODO: Use python-apt to use the real repository data (and manage correct checksum) |
290 | + # Right now this manage only the official jenkins repository layout, which is non-standard |
291 | + self.jenkins_repo = jenkins_repo |
292 | + if jenkins_release not in ("current", "stable"): |
293 | + err = "Wrong jenkins_release parameter. Valid values are [current|stable]" |
294 | + logger.error(err) |
295 | + raise ValueError(err) |
296 | + self.jenkins_release = jenkins_release |
297 | + self.latest_core_url = "{0}/{1}/latestCore.txt".format(jenkins_core_url, jenkins_release) |
298 | + self.core_version = self.load_latest_core_version(self.latest_core_url) |
299 | + |
300 | + def load_latest_core_version(self, core_url): |
301 | + """Load the latest version available. |
302 | + |
303 | + :param str core_url: Url of the latest Jenkins core |
304 | + """ |
305 | + logger.info("Loading %s", core_url) |
306 | + with urllib.request.urlopen(core_url) as core_data: |
307 | + core_version = core_data.read().decode("utf-8") |
308 | + logger.debug("Latest jenkins core version: %s", core_version) |
309 | + return core_version |
310 | + |
311 | + def get_binary_package(self, dst_dir, distribution="debian", version=None, arch="amd64"): |
312 | + """Get a Jenkins binary package. |
313 | + |
314 | + Download a binary package of the said distribution and of the latest Jenkins |
315 | + release if not specified. |
316 | + |
317 | + :param str dst_dir: Target directory for the download |
318 | + :param str distribution: Distribution family to retrieve package for (debian, redhat) |
319 | + :param str version: The version you want to download. Default to the latest Core if not specified |
320 | + :param str arch: Architecture to download. Default to amd64 |
321 | + """ |
322 | + |
323 | + assert distribution in ("debian", "redhat") |
324 | + if self.jenkins_release == "stable": |
325 | + repo_dist = "{0}-stable".format(distribution) |
326 | + else: |
327 | + repo_dist = distribution |
328 | + |
329 | + if not version: |
330 | + target_version = self.core_version |
331 | + else: |
332 | + target_version = version |
333 | + binary_package_url = "{url}/{dist}/binary/jenkins_{version}_all.deb".format( |
334 | + url=self.jenkins_repo, dist=repo_dist, version=target_version |
335 | + ) |
336 | + package_file = os.path.basename(binary_package_url) |
337 | + package_file_path = os.path.join(dst_dir, package_file) |
338 | + with urllib.request.urlopen(binary_package_url) as binary_pkg: |
339 | + if not os.path.exists(os.path.dirname(package_file_path)): |
340 | + os.makedirs(os.path.dirname(package_file_path)) |
341 | + logger.info("Downloading %s to %s", binary_package_url, package_file_path) |
342 | + with open(package_file_path, "wb") as target_pkg: |
343 | + target_pkg.write(binary_pkg.read()) |
344 | + logger.info("%s downloaded", package_file) |
345 | diff --git a/jenkins_plugin_manager/exceptions.py b/jenkins_plugin_manager/exceptions.py |
346 | new file mode 100644 |
347 | index 0000000..e7871bb |
348 | --- /dev/null |
349 | +++ b/jenkins_plugin_manager/exceptions.py |
350 | @@ -0,0 +1,19 @@ |
351 | +"""Jenkins Plugin Manager exceptions.""" |
352 | + |
353 | + |
354 | +class InvalidPluginVersionError(Exception): |
355 | + """Exception when a desired plugin is higher than the most recent one in the Update Center.""" |
356 | + |
357 | + pass |
358 | + |
359 | + |
360 | +class InvalidPluginError(Exception): |
361 | + """Exception when the desired plugin is invalid or not available.""" |
362 | + |
363 | + pass |
364 | + |
365 | + |
366 | +class PluginMinCoreVersionError(Exception): |
367 | + """Exception when a plugin need a higher jenkins core version than the one set as the current one.""" |
368 | + |
369 | + pass |
370 | diff --git a/jenkins_plugin_manager/main.py b/jenkins_plugin_manager/main.py |
371 | new file mode 100755 |
372 | index 0000000..155dde0 |
373 | --- /dev/null |
374 | +++ b/jenkins_plugin_manager/main.py |
375 | @@ -0,0 +1,122 @@ |
376 | +#!/usr/bin/env python3 |
377 | +""" |
378 | +Entrypoint for the jenkins-plugin-manager module |
379 | +""" |
380 | + |
381 | +import argparse |
382 | +import logging |
383 | +import sys |
384 | + |
385 | +from jenkins_plugin_manager.plugin import UpdateCenter |
386 | + |
387 | + |
388 | +logger = logging.getLogger("jenkins_plugin_manager") |
389 | + |
390 | + |
391 | +def parse_args(): |
392 | + """Parse the arguments provided to the CLI.""" |
393 | + parser = argparse.ArgumentParser(prog="jenkins-plugin-manager") |
394 | + parser.add_argument("--log-level", action="store", help="Logging level", default="WARNING") |
395 | + parser.add_argument("--log-file", action="store", help="Log file") |
396 | + parser.add_argument("--log-file-level", action="store", help="File logging level", default="INFO") |
397 | + subparsers = parser.add_subparsers(help="sub-command help") |
398 | + |
399 | + # Sub command "plugin" |
400 | + parser_plugin = subparsers.add_parser("plugin", help="Manage Jenkins plugins") |
401 | + parser_plugin.add_argument("--from-file", action="store", help="Load a list of plugin from a file") |
402 | + subparser_plugin = parser_plugin.add_subparsers(help="plugin sub-command help") |
403 | + |
404 | + # Sub command "plugin list-urls" |
405 | + parser_plugin_list = subparser_plugin.add_parser("list-urls", help="List Jenkins plugins url") |
406 | + parser_plugin_list.add_argument("args", nargs=argparse.REMAINDER, help="List of plugin") |
407 | + parser_plugin_list.set_defaults(func=plugin_list_urls) |
408 | + |
409 | + # Sub command "plugin download" |
410 | + parser_plugin_download = subparser_plugin.add_parser("download", help="Download Jenkins plugins") |
411 | + parser_plugin_download.add_argument( |
412 | + "--dst", action="store", required=True, help="Directory for the downloaded plugins" |
413 | + ) |
414 | + parser_plugin_download.add_argument("args", nargs=argparse.REMAINDER, help="List of plugin") |
415 | + parser_plugin_download.set_defaults(func=plugin_download) |
416 | + |
417 | + return parser.parse_args() |
418 | + |
419 | + |
420 | +def configure_logging(args): |
421 | + """Configure the logging. |
422 | + |
423 | + :param :py:class:argparse.ArgumentParser args: Arguments provided |
424 | + """ |
425 | + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") |
426 | + logger.setLevel(logging.DEBUG) |
427 | + log_level = getattr(logging, args.log_level.upper(), None) |
428 | + if not isinstance(log_level, int): |
429 | + raise ValueError("Invalid log-level level: %s" % args.log_level) |
430 | + console = logging.StreamHandler() |
431 | + console.setLevel(log_level) |
432 | + console.setFormatter(formatter) |
433 | + logger.addHandler(console) |
434 | + if args.log_file: |
435 | + log_file_level = getattr(logging, args.log_file_level.upper(), None) |
436 | + if not isinstance(log_file_level, int): |
437 | + raise ValueError("Invalid --log-file-level level: %s" % args.log_file_level) |
438 | + logfile_handler = logging.FileHandler(args.log_file) |
439 | + logfile_handler.setLevel(log_file_level) |
440 | + logfile_handler.setFormatter(formatter) |
441 | + logger.addHandler(logfile_handler) |
442 | + |
443 | + |
444 | +def plugin_load(args): |
445 | + """Return the list of plugin provided by the options. |
446 | + |
447 | + :param :py:class:argparse.ArgumentParser args: Arguments provided |
448 | + """ |
449 | + plugins = [] |
450 | + if args.from_file: |
451 | + try: |
452 | + with open(args.from_file, "r") as plugin_file: |
453 | + plugins.extend(plugin_file.read().splitlines()) |
454 | + except FileNotFoundError: |
455 | + logger.error("File %s: Not found", args.from_file) |
456 | + sys.exit(1) |
457 | + if args.args: |
458 | + plugins.extend(args.args) |
459 | + if not plugins: |
460 | + logger.error("No plugin provided.") |
461 | + sys.exit(1) |
462 | + return plugins |
463 | + |
464 | + |
465 | +def plugin_download(args): |
466 | + """Download the given plugins. |
467 | + |
468 | + :param :py:class:argparse.ArgumentParser args: Arguments provided |
469 | + """ |
470 | + plugins = plugin_load(args) |
471 | + uc = UpdateCenter() |
472 | + for plugin in uc.get_plugins(set(plugins)): |
473 | + print(uc.download_plugin(plugin, args.dst)) |
474 | + |
475 | + |
476 | +def plugin_list_urls(args): |
477 | + """Sub-command "plugin list-urls". |
478 | + |
479 | + Print the url of each plugin that need is specified. |
480 | + |
481 | + :param :py:class:argparse.ArgumentParser args: Arguments provided |
482 | + """ |
483 | + plugins = plugin_load(args) |
484 | + uc = UpdateCenter() |
485 | + for plugin in uc.get_plugins(set(plugins)): |
486 | + print(uc.get_plugin_data(plugin)["url"]) |
487 | + |
488 | + |
489 | +def main(): |
490 | + """Main entrypoint for jenkins-plugin-manager CLI tool.""" |
491 | + args = parse_args() |
492 | + configure_logging(args) |
493 | + args.func(args) |
494 | + |
495 | + |
496 | +if __name__ == "__main__": |
497 | + main() |
498 | diff --git a/jenkins_plugin_manager/plugin.py b/jenkins_plugin_manager/plugin.py |
499 | new file mode 100644 |
500 | index 0000000..84b6c19 |
501 | --- /dev/null |
502 | +++ b/jenkins_plugin_manager/plugin.py |
503 | @@ -0,0 +1,216 @@ |
504 | +#!/usr/bin/env python3 |
505 | + |
506 | +""" |
507 | +Jenkins Plugin Manager plugin module. |
508 | + |
509 | +This module loads the JSON file available in jenkins Update Center |
510 | +(default "https://updates.jenkins.io/stable/update-center.json") and load it for further computation. |
511 | +""" |
512 | + |
513 | +import base64 |
514 | +import codecs |
515 | +import hashlib |
516 | +import json |
517 | +import logging |
518 | +import os |
519 | +import urllib.request |
520 | + |
521 | +from pkg_resources import parse_version |
522 | + |
523 | +from jenkins_plugin_manager.exceptions import ( |
524 | + InvalidPluginVersionError, |
525 | + InvalidPluginError, |
526 | + PluginMinCoreVersionError, |
527 | +) |
528 | + |
529 | +logger = logging.getLogger(__name__) |
530 | + |
531 | + |
532 | +class UpdateCenter(object): |
533 | + """Represents the UpdateCenter data.""" |
534 | + |
535 | + def __init__( |
536 | + self, uc_url="https://updates.jenkins.io/stable/update-center.json", |
537 | + ): |
538 | + """Instantiate the UpdateCenter class. |
539 | + |
540 | + ..code:: python |
541 | + |
542 | + uc = jenkins_plugin_manager.UpdateCenter() |
543 | + for plugin in uc.get_plugins(["ansicolor", "docker-workflow"]): |
544 | + uc.download_plugin(plugin, "/var/lib/jenkins/plugins", with_version=False |
545 | + |
546 | + :param str uc_url: The url of the UpdateCenter JSON file. Default to the official stable one. |
547 | + """ |
548 | + self.uc_data = self.load_update_center_data(uc_url) |
549 | + |
550 | + def load_update_center_data(self, uc_url): |
551 | + """Load the content of the Update Center JSON file. |
552 | + |
553 | + :param str uc_url: Update Center JSON file url. |
554 | + """ |
555 | + logger.info("Loading Jenkins Update Center data from %s", uc_url) |
556 | + with urllib.request.urlopen(uc_url) as uc_data: |
557 | + # Remove the "updateCenter.post(\n ... \n);" around the json payload |
558 | + json_payload = uc_data.read().decode("utf-8").lstrip("updateCenter.post(\n").rstrip("\n);") |
559 | + return json.loads(json_payload) |
560 | + |
561 | + def get_plugin_data(self, plugin): |
562 | + """Return the specific plugin data. |
563 | + |
564 | + The plugin can be specificed with "latest" of it's version in the form "<plugin>:(<version>|latest)". |
565 | + However, only the latest plugin is pulled. A check is performed to ensure the desired version is inferior or |
566 | + equal to the latest available. |
567 | + |
568 | + :param str plugin: The name of the plugin, possibly with the version or "latest". e.g <plugin>:<version>. |
569 | + :returns: The dictionary of the plugin's data as seen in the Update Center JSON file. |
570 | + :rtype: dict |
571 | + """ |
572 | + if ":" in plugin: |
573 | + plugin_name, plugin_version = plugin.split(":") |
574 | + else: |
575 | + plugin_name = plugin |
576 | + plugin_version = "latest" |
577 | + |
578 | + try: |
579 | + if plugin_version != "latest": |
580 | + # Check that the desired version is <= to the one available in the latest verison of the plugin |
581 | + latest_plugin_version = self.uc_data["plugins"][plugin_name]["version"] |
582 | + if parse_version(plugin_version) > parse_version(latest_plugin_version): |
583 | + err = "Plugin {0} version {1} is higher than the latest available version {2}".format( |
584 | + plugin_name, plugin_version, latest_plugin_version |
585 | + ) |
586 | + logger.error(err) |
587 | + raise InvalidPluginVersionError(err) |
588 | + elif parse_version(plugin_version) < parse_version(latest_plugin_version): |
589 | + logger.info("Getting %s:%s instead of %s", plugin_name, latest_plugin_version, plugin) |
590 | + |
591 | + plugin_data = self.uc_data["plugins"][plugin_name] |
592 | + logger.debug("Plugin %s data: %s", plugin_name, plugin_data) |
593 | + except KeyError: |
594 | + err = "Cannot find plugin {0}".format(plugin_name) |
595 | + logger.error(err) |
596 | + raise InvalidPluginError(err) from None |
597 | + return plugin_data |
598 | + |
599 | + def _verify_sha256sum(self, plugin_file_path, sha256sum): |
600 | + """Verify the sha256sum of the given file. |
601 | + |
602 | + :param str plugin_file_path: Path of the downloaded file. |
603 | + :param str sha256sum: A base64 encoding of the sha256sum of the plugin. |
604 | + :returns: True if the file has the right content, False otherwise. |
605 | + :rtype: bool |
606 | + """ |
607 | + # The sha256 given is a binary base64 that need to be encoded to hex then converted into a string |
608 | + # See https://github.com/jenkins-infra/update-center2/blob/master/src/main/java/org/jvnet/hudson/update_center/IndexHtmlBuilder.java#L82 # noqa |
609 | + sha256sum_hex = codecs.decode(codecs.encode(base64.b64decode(sha256sum), "hex"), "utf-8") |
610 | + try: |
611 | + with open(plugin_file_path, "rb") as plugin_file: |
612 | + content = plugin_file.read() |
613 | + sha256sum_check = hashlib.sha256(content).hexdigest() |
614 | + if sha256sum_check != sha256sum_hex: |
615 | + logger.error("SHA256sum check failed, found %s (expected %s)", sha256sum_check, sha256sum_hex) |
616 | + return False |
617 | + except FileNotFoundError: |
618 | + logger.error("Cannot open the file %s", plugin_file_path) |
619 | + return False |
620 | + return True |
621 | + |
622 | + def _check_min_core_version(self, core_version, plugin_min_core_version): |
623 | + """Check the plugin required Jenkins core version. |
624 | + |
625 | + :param str core_version: Version of Jenkins core. |
626 | + :param str plugin_min_core_version: The minimum version of Jenkins core required for this plugin. |
627 | + :returns: True if the Jenkins core version is sufficient, False otherwise. |
628 | + :rtype: bool |
629 | + """ |
630 | + if parse_version(core_version) < parse_version(plugin_min_core_version): |
631 | + logger.error( |
632 | + "Jenkins core version %s or above is required (%s found)", plugin_min_core_version, core_version |
633 | + ) |
634 | + return False |
635 | + return True |
636 | + |
637 | + def download_plugin(self, plugin, dst_dir, plugin_url=None, with_version=True): |
638 | + """Download the given plugin in the "dst_dir" directory. |
639 | + |
640 | + A checksum is done on the files downloaded. |
641 | + |
642 | + :param str plugin: The name of the plugin, possibly with the version or "latest". e.g <plugin>:<version>. |
643 | + :param str dst_dir: A path where the files are downloaded. |
644 | + :param str plugin_url: If set, use this url to retrieve the plugin instead of the one in the UpdateCenter |
645 | + JSON file. This is useful when you use a in-house HTTP server hosting the plugins without having |
646 | + a Jenkins updateCenter. |
647 | + :param bool with_version: Append the version of the plugin to the name in the target file if True. |
648 | + Only the name of the plugin if False. |
649 | + :returns: The path of the downloaded file, False if it fails. |
650 | + :rtype: str |
651 | + """ |
652 | + plugin_data = self.get_plugin_data(plugin) |
653 | + plugin_name = plugin_data["name"] |
654 | + |
655 | + if not plugin_url: |
656 | + plugin_url = plugin_data["url"] |
657 | + |
658 | + logger.info("Downloading plugin %s from %s", plugin_name, plugin_url) |
659 | + with urllib.request.urlopen(plugin_url) as final_plugin_url: |
660 | + if with_version: |
661 | + plugin_file_path = os.path.join(dst_dir, "{0}-{1}.jpi".format(plugin_name, plugin_data["version"])) |
662 | + else: |
663 | + plugin_file_path = os.path.join(dst_dir, "{0}.jpi".format(plugin_name)) |
664 | + if not os.path.exists(os.path.dirname(plugin_file_path)): |
665 | + os.makedirs(os.path.dirname(plugin_file_path)) |
666 | + with open(plugin_file_path, "wb") as plugin_file: |
667 | + plugin_file.write(final_plugin_url.read()) |
668 | + if not self._verify_sha256sum(plugin_file_path, plugin_data["sha256"]): |
669 | + return False |
670 | + logger.info("Plugin %s downloaded to %s", plugin_name, plugin_file_path) |
671 | + return plugin_file_path |
672 | + |
673 | + def get_plugins_dependencies(self, plugin, optional=False): |
674 | + """Get the list of the plugin's dependencies. |
675 | + |
676 | + :param str plugin: The name of the plugin, possibly with the version or "latest". e.g <plugin>:<version>. |
677 | + :param str dst: A path where the files are downloaded. |
678 | + :param bool optional: Get the optional dependencies. |
679 | + :returns: A set of the plugin's dependency. |
680 | + :rtype: set |
681 | + """ |
682 | + plugin_data = self.get_plugin_data(plugin) |
683 | + dependencies_to_fetch = set() |
684 | + for dependency in plugin_data["dependencies"]: |
685 | + # Skip optional dependency or if already in the list |
686 | + if dependency["optional"] and not optional: |
687 | + continue |
688 | + # Since the constraints are always >=, we don't care to pick up the latest dependency |
689 | + dependencies_to_fetch.add(dependency["name"]) |
690 | + return dependencies_to_fetch |
691 | + |
692 | + def get_plugins(self, plugins, current_core_version=None, with_dependency=True, optional=False): |
693 | + """Get a list of plugins with their dependencies. |
694 | + |
695 | + You can also add optional dependencies if needed. |
696 | + |
697 | + :param list plugins: List of plugins to download. Each plugin is a string containing the name of the plugin and |
698 | + possibly a colon (":") with the version of the plugin or "latest". If omitted, "latest" is assumed. |
699 | + :param str current_core_version: Jenkins core version running. |
700 | + :param bool optional: Add the optional dependencies if True. |
701 | + :returns: A set of plugins to download. |
702 | + :rtype: set |
703 | + """ |
704 | + plugins_to_fetch = set() |
705 | + dependencies_to_fetch = set() |
706 | + for plugin in plugins: |
707 | + plugin_data = self.get_plugin_data(plugin) |
708 | + plugin_name = plugin_data["name"] |
709 | + # Check if the Jenkins Core version is enough |
710 | + if current_core_version: |
711 | + logger.info("Checking minimum Jenkins core version required for %s", plugin) |
712 | + if not self._check_min_core_version(current_core_version, plugin_data["requiredCore"]): |
713 | + raise PluginMinCoreVersionError( |
714 | + "Plugin {0} requires Jenkins core version {1}".format(plugin_name, plugin_data["requiredCore"]) |
715 | + ) |
716 | + plugins_to_fetch.add(plugin_data["name"]) |
717 | + if with_dependency: |
718 | + dependencies_to_fetch = dependencies_to_fetch.union(self.get_plugins_dependencies(plugin, optional)) |
719 | + return plugins_to_fetch.union(dependencies_to_fetch) |
720 | diff --git a/requirements.txt b/requirements.txt |
721 | new file mode 100644 |
722 | index 0000000..4e92b9d |
723 | --- /dev/null |
724 | +++ b/requirements.txt |
725 | @@ -0,0 +1,2 @@ |
726 | +black |
727 | +flake8 |
728 | diff --git a/setup.py b/setup.py |
729 | new file mode 100644 |
730 | index 0000000..3659cd2 |
731 | --- /dev/null |
732 | +++ b/setup.py |
733 | @@ -0,0 +1,44 @@ |
734 | +"""Jenkins Plugin Manager module.""" |
735 | + |
736 | +# Always prefer setuptools over distutils |
737 | +from setuptools import setup, find_packages |
738 | +from os import path |
739 | + |
740 | +here = path.abspath(path.dirname(__file__)) |
741 | + |
742 | +# Get the long description from the README file |
743 | +with open(path.join(here, "README.md"), encoding="utf-8") as f: |
744 | + long_description = f.read() |
745 | + |
746 | +# Arguments marked as "Required" below must be included for upload to PyPI. |
747 | +# Fields marked as "Optional" may be commented out. |
748 | + |
749 | +setup( |
750 | + name="jenkins-plugin-manager", # Required |
751 | + version="0.3", # Required |
752 | + description="Manage Jenkins plugin (get dependencies, download them)", # Optional |
753 | + long_description=long_description, # Optional |
754 | + long_description_content_type="text/markdown", # Optional (see note above) |
755 | + url="https://launchpad.net/jenkins-plugin-manager", # Optional |
756 | + author="Benjamin Allot", # Optional |
757 | + author_email="benjamin.allot@canonical.com", # Optional |
758 | + classifiers=[ # Optional |
759 | + "Development Status :: 3 - Alpha", |
760 | + "Intended Audience :: Developers", |
761 | + "Topic :: Software Development :: Build Tools", |
762 | + "License :: OSI Approved :: MIT License", |
763 | + "Programming Language :: Python :: 3", |
764 | + ], |
765 | + keywords="jenkins", # Optional |
766 | + packages=find_packages(exclude=["tests"]), # Required |
767 | + install_requires="setuptools", |
768 | + python_requires=">=3.3", |
769 | + extras_require={"test": ["coverage"]}, # Optional |
770 | + # For example, the following would provide a command called `sample` which |
771 | + # executes the function `main` from this package when invoked: |
772 | + entry_points={"console_scripts": ["jenkins-plugin-manager=jenkins_plugin_manager.main:main"]}, # Optional |
773 | + project_urls={ # Optional |
774 | + "Bug Reports": "https://bugs.launchpad.net/jenkins-plugin-manager", |
775 | + "Source": "https://code.launchpad.net/jenkins-plugin-manager", |
776 | + }, |
777 | +) |
778 | diff --git a/tests/test_jenkins_plugin_manager_core.py b/tests/test_jenkins_plugin_manager_core.py |
779 | new file mode 100644 |
780 | index 0000000..79d1119 |
781 | --- /dev/null |
782 | +++ b/tests/test_jenkins_plugin_manager_core.py |
783 | @@ -0,0 +1,52 @@ |
784 | +import unittest |
785 | + |
786 | +from unittest import mock |
787 | + |
788 | +from jenkins_plugin_manager.core import ( |
789 | + JenkinsCore, |
790 | + DEFAULT_JENKINS_RELEASE, |
791 | + DEFAULT_JENKINS_CORE_URL, |
792 | +) |
793 | + |
794 | + |
795 | +class TestJenkinsCore(unittest.TestCase): |
796 | + """Tests for the JenkinsCore class.""" |
797 | + |
798 | + LATEST_CORE_VERSION = "2.1.190" |
799 | + |
800 | + def setUp(self): |
801 | + """Setup fxture for each test.""" |
802 | + core_version_patch = mock.patch("jenkins_plugin_manager.core.JenkinsCore.load_latest_core_version") |
803 | + self.core_version_mock = core_version_patch.start() |
804 | + self.addCleanup(core_version_patch.stop) |
805 | + self.addCleanup(self.core_version_mock.reset_mock) |
806 | + self.core_version_mock.return_value = self.LATEST_CORE_VERSION |
807 | + self.jenkins_core = JenkinsCore() |
808 | + |
809 | + def test_jenkinscore_attr(self): |
810 | + """Check the varios attributes of the class.""" |
811 | + # Check default attribute |
812 | + self.assertEqual(self.jenkins_core.jenkins_release, DEFAULT_JENKINS_RELEASE) |
813 | + self.assertEqual( |
814 | + self.jenkins_core.latest_core_url, |
815 | + "{0}/{1}/latestCore.txt".format(DEFAULT_JENKINS_CORE_URL, DEFAULT_JENKINS_RELEASE), |
816 | + ) |
817 | + self.assertEqual(self.jenkins_core.core_version, self.LATEST_CORE_VERSION) |
818 | + |
819 | + # Check custom repository |
820 | + jenkins_repo = "http://pkg.jenkins.canonical.com" |
821 | + jenkins_release = "current" |
822 | + jenkins_core_url = "http://updates.jenkins.canonical.com" |
823 | + custom_jc = JenkinsCore( |
824 | + jenkins_release=jenkins_release, jenkins_repo=jenkins_repo, jenkins_core_url=jenkins_core_url |
825 | + ) |
826 | + self.assertEqual(custom_jc.jenkins_release, jenkins_release) |
827 | + self.assertEqual(custom_jc.latest_core_url, "{0}/{1}/latestCore.txt".format(jenkins_core_url, jenkins_release)) |
828 | + |
829 | + # Check exception |
830 | + jenkins_release = "custom" |
831 | + with self.assertRaises(ValueError) as cm: |
832 | + custom_jc = JenkinsCore(jenkins_release=jenkins_release) |
833 | + self.assertEqual( |
834 | + cm.exception, ValueError("Wrong jenkins_release parameter. Valid values are [current|stable]") |
835 | + ) |
836 | diff --git a/tests/test_jenkins_plugin_manager_plugin.py b/tests/test_jenkins_plugin_manager_plugin.py |
837 | new file mode 100644 |
838 | index 0000000..768a7ff |
839 | --- /dev/null |
840 | +++ b/tests/test_jenkins_plugin_manager_plugin.py |
841 | @@ -0,0 +1,176 @@ |
842 | +import json |
843 | +import os |
844 | +import tempfile |
845 | +import unittest |
846 | + |
847 | +from unittest import mock |
848 | + |
849 | +import jenkins_plugin_manager |
850 | + |
851 | +from jenkins_plugin_manager.plugin import UpdateCenter |
852 | + |
853 | +UC_DATA_FILE = "update_center_data.json" |
854 | + |
855 | + |
856 | +class TestUpdateCenterLoad(unittest.TestCase): |
857 | + """Tests for the UpdateCenter methods loading data.""" |
858 | + |
859 | + dir_path = os.path.dirname(os.path.realpath(__file__)) |
860 | + uc_data_file = os.path.join(dir_path, UC_DATA_FILE) |
861 | + with open(uc_data_file, "r") as uc_data: |
862 | + uc_data = uc_data.read() |
863 | + |
864 | + @mock.patch("urllib.request.urlopen", autospec=True) |
865 | + def test_load_update_center_data(self, urllib_mock): |
866 | + cm = mock.MagicMock() |
867 | + cm.read.return_value = "updateCenter.post(\n{0}\n);".format(self.uc_data).encode("utf-8") |
868 | + cm.__enter__.return_value = cm |
869 | + urllib_mock.return_value = cm |
870 | + uc = UpdateCenter() |
871 | + self.assertEqual(uc.uc_data, json.loads(self.uc_data)) |
872 | + |
873 | + |
874 | +class TestUpdateCenter(unittest.TestCase): |
875 | + """Tests for the UpdateCenter class and methods.""" |
876 | + |
877 | + dir_path = os.path.dirname(os.path.realpath(__file__)) |
878 | + uc_data_file = os.path.join(dir_path, UC_DATA_FILE) |
879 | + with open(uc_data_file, "r") as uc_data: |
880 | + uc_data = json.loads(uc_data.read()) |
881 | + |
882 | + def setUp(self): |
883 | + """Load samples of the Jenkins UpdateCenter JSON files.""" |
884 | + uc_data_patch = mock.patch("jenkins_plugin_manager.plugin.UpdateCenter.load_update_center_data") |
885 | + self.uc_data_mock = uc_data_patch.start() |
886 | + self.addCleanup(self.uc_data_mock.reset_mock) |
887 | + self.addCleanup(uc_data_patch.stop) |
888 | + self.uc_data_mock.return_value = self.uc_data |
889 | + self.uc = UpdateCenter() |
890 | + |
891 | + def test_default_load_data(self): |
892 | + """Test the default JSON files.""" |
893 | + self.uc_data_mock.assert_called_once_with("https://updates.jenkins.io/stable/update-center.json") |
894 | + |
895 | + self.uc_data_mock.reset_mock() |
896 | + |
897 | + @mock.patch("jenkins_plugin_manager.plugin.UpdateCenter.load_update_center_data") |
898 | + def test_custom_load_data(self, custom_uc_data_mock): |
899 | + """Test custom urls for JSON files.""" |
900 | + custom_uc_data_mock.return_value = self.uc_data |
901 | + UpdateCenter(uc_url="http://archive.admin.canonical.com/others/stable/update-center.json",) |
902 | + custom_uc_data_mock.assert_called_once_with( |
903 | + "http://archive.admin.canonical.com/others/stable/update-center.json" |
904 | + ) |
905 | + |
906 | + def test_get_plugin_data(self): |
907 | + """Test the plugin data and the exception raised.""" |
908 | + plugin_test = "ansible" |
909 | + plugin_data = self.uc.get_plugin_data(plugin_test) |
910 | + self.assertEqual(plugin_data, self.uc_data["plugins"][plugin_test]) |
911 | + plugin_test = "PluginThatDoesNotExist" |
912 | + with self.assertRaises(jenkins_plugin_manager.exceptions.InvalidPluginError): |
913 | + self.uc.get_plugin_data(plugin_test) |
914 | + plugin_test = "docker-workflow:999999" |
915 | + with self.assertRaises(jenkins_plugin_manager.exceptions.InvalidPluginVersionError): |
916 | + plugin_data = self.uc.get_plugin_data(plugin_test) |
917 | + plugin_test = "docker-workflow:1.20" |
918 | + with self.assertLogs(logger="jenkins_plugin_manager", level="INFO") as cm: |
919 | + plugin_data = self.uc.get_plugin_data(plugin_test) |
920 | + self.assertEqual(plugin_data["version"], "1.21") |
921 | + self.assertEqual( |
922 | + cm.output, |
923 | + ["INFO:jenkins_plugin_manager.plugin:Getting docker-workflow:1.21 instead of docker-workflow:1.20"], |
924 | + ) |
925 | + |
926 | + def test_verify_sha256sum(self): |
927 | + """Test the weird sha256sum. |
928 | + |
929 | + The sha256sum provided in the Update Center is a weird one. |
930 | + See https://github.com/jenkins-infra/update-center2/blob/master/src/main/java/org/jvnet/hudson/update_center/IndexHtmlBuilder.java#L82 # noqa |
931 | + Fake a file containing "Jenkins Plugin Manager Rocks!\n" for the check. |
932 | + The sha256sum is "0290683d8fa6746e9d6de82d0ab8753a5e4078d5c2a9a40aa08f1a0c05429748". |
933 | + The sha256sum expected as argument is "ApBoPY+mdG6dbegtCrh1Ol5AeNXCqaQKoI8aDAVCl0g=". |
934 | + """ |
935 | + with tempfile.TemporaryDirectory() as test_dir: |
936 | + stub_file = os.path.join(test_dir, "check") |
937 | + with open(stub_file, "w") as test_file: |
938 | + test_file.write("Jenkins Plugin Manager Rocks!\n") |
939 | + check = self.uc._verify_sha256sum(stub_file, "ApBoPY+mdG6dbegtCrh1Ol5AeNXCqaQKoI8aDAVCl0g=") |
940 | + self.assertTrue(check) |
941 | + |
942 | + def test_check_min_core_version(self): |
943 | + """Test the required Jenkins core version is available.""" |
944 | + with self.assertLogs(logger="jenkins_plugin_manager.plugin", level="ERROR") as cm: |
945 | + self.assertFalse(self.uc._check_min_core_version("2.1", "2.190.1")) |
946 | + self.assertEqual( |
947 | + cm.output, |
948 | + ["ERROR:jenkins_plugin_manager.plugin:Jenkins core version 2.190.1 or above is required (2.1 found)"], |
949 | + ) |
950 | + self.assertTrue(self.uc._check_min_core_version("2.190.1", "2.190")) |
951 | + |
952 | + @mock.patch("jenkins_plugin_manager.plugin.UpdateCenter.get_plugin_data") |
953 | + @mock.patch("jenkins_plugin_manager.plugin.UpdateCenter._verify_sha256sum", autospec=True) |
954 | + @mock.patch("urllib.request.urlopen", autospec=True) |
955 | + def test_download_plugin(self, urllib_mock, checksum_mock, get_plugin_data_mock): |
956 | + """Test the download of a plugin.""" |
957 | + |
958 | + def get_plugin_data(plugin): |
959 | + return self.uc_data["plugins"][plugin] |
960 | + |
961 | + cm = mock.MagicMock() |
962 | + cm.read.return_value = b"This is a plugin" |
963 | + cm.__enter__.return_value = cm |
964 | + urllib_mock.return_value = cm |
965 | + checksum_mock.return_value = True |
966 | + get_plugin_data_mock.side_effect = get_plugin_data |
967 | + with tempfile.TemporaryDirectory() as test_dir: |
968 | + plugin_file = os.path.join(test_dir, "jenkins", "ansicolor-0.6.2.jpi") |
969 | + with self.assertLogs(logger="jenkins_plugin_manager.plugin", level="INFO") as cm: |
970 | + filepath = self.uc.download_plugin("ansicolor", os.path.join(test_dir, "jenkins")) |
971 | + self.assertTrue(os.path.exists(os.path.join(test_dir, "jenkins"))) |
972 | + self.assertEqual(filepath, plugin_file) |
973 | + self.assertEqual( |
974 | + cm.output, |
975 | + [ |
976 | + "INFO:jenkins_plugin_manager.plugin:Downloading plugin {0} from {1}".format( |
977 | + "ansicolor", "http://updates.jenkins-ci.org/download/plugins/ansicolor/0.6.2/ansicolor.hpi" |
978 | + ), |
979 | + "INFO:jenkins_plugin_manager.plugin:Plugin ansicolor downloaded to {0}".format(plugin_file), |
980 | + ], |
981 | + ) |
982 | + # Check that when verify_sha256sum fails, return False |
983 | + checksum_mock.return_value = False |
984 | + with self.assertLogs(logger="jenkins_plugin_manager.plugin", level="INFO") as cm: |
985 | + self.assertFalse( |
986 | + self.uc.download_plugin( |
987 | + "ant", |
988 | + os.path.join(test_dir, "jenkins"), |
989 | + plugin_url="http://dummy.canonical.com/ant.hpi", |
990 | + with_version=False, |
991 | + ) |
992 | + ) |
993 | + self.assertEqual( |
994 | + cm.output, |
995 | + [ |
996 | + "INFO:jenkins_plugin_manager.plugin:Downloading plugin {0} from {1}".format( |
997 | + "ant", "http://dummy.canonical.com/ant.hpi" |
998 | + ) |
999 | + ], |
1000 | + ) |
1001 | + self.assertFalse(os.path.exists(os.path.join(test_dir, "jenkins", "ant-1.10.jpi"))) |
1002 | + self.assertTrue(os.path.exists(os.path.join(test_dir, "jenkins", "ant.jpi"))) |
1003 | + |
1004 | + def test_get_plugin_dependencies(self): |
1005 | + """Test the list of dependencies for a given plugin.""" |
1006 | + # Should be empty because all dependencies are optional |
1007 | + deps = self.uc.get_plugins_dependencies("ansicolor") |
1008 | + self.assertEqual(set(), deps) |
1009 | + deps = self.uc.get_plugins_dependencies("ansicolor", optional=True) |
1010 | + self.assertEqual({"workflow-api", "workflow-step-api"}, deps) |
1011 | + |
1012 | + def test_get_plugins(self): |
1013 | + """Test the list of plugins to fetch and the check with the current Jenkins core version.""" |
1014 | + with self.assertRaises(jenkins_plugin_manager.exceptions.PluginMinCoreVersionError): |
1015 | + self.uc.get_plugins(["ansicolor"], current_core_version="2.144") |
1016 | + plugins = self.uc.get_plugins(["ant"]) |
1017 | + self.assertEqual(plugins, {"ant", "structs"}) |
1018 | diff --git a/tox.ini b/tox.ini |
1019 | new file mode 100644 |
1020 | index 0000000..e94d726 |
1021 | --- /dev/null |
1022 | +++ b/tox.ini |
1023 | @@ -0,0 +1,36 @@ |
1024 | +[tox] |
1025 | +skipsdist=True |
1026 | +envlist = unit |
1027 | +skip_missing_interpreters = True |
1028 | + |
1029 | +[testenv] |
1030 | +basepython = python3 |
1031 | +setenv = |
1032 | + PYTHONPATH = {toxinidir} |
1033 | + |
1034 | +[testenv:unit] |
1035 | +commands = |
1036 | + pytest \ |
1037 | + {posargs:-v --cov=jenkins_plugin_manager --cov-report=term-missing --cov-branch} |
1038 | +deps = |
1039 | + -r{toxinidir}/requirements.txt |
1040 | + pytest |
1041 | + pytest-coverage |
1042 | + |
1043 | +[testenv:black] |
1044 | +commands = |
1045 | + black --skip-string-normalization --line-length=120 {toxinidir} |
1046 | +deps = black |
1047 | + |
1048 | +[testenv:lint] |
1049 | +commands = |
1050 | + flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,.venv |
1051 | +deps = flake8 |
1052 | + |
1053 | +[flake8] |
1054 | +exclude = |
1055 | + .git, |
1056 | + __pycache__, |
1057 | + .tox, |
1058 | +max-line-length = 120 |
1059 | +max-complexity = 10 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.