From 591a55aad5cbfc7c2c92a3c7350d6ef858427eeb Mon Sep 17 00:00:00 2001 From: Christian Kellner Date: Mon, 2 May 2022 15:25:20 +0200 Subject: [PATCH] plugins: add support for customizations The Cloud API supports passing in a variety of image customizations, like e.g. extra packages or pre-defining users. Add a new command line option to the client `--customizations` which takes a path to a JSON file which contains the customziations; they will be passed via the existing `opts` argument to the hub. Add support for `customizations` to the `opts`/`options` arguments to the hub plugin. No validation to the object is done. Instead we rely in Composer for the validation of the content. Add support for `customizations` the image `ComposeRequest` in the builder plugin. All specified values are just passed through to composer as-is. Add tests for the respective plugins. --- plugins/builder/osbuild.py | 9 +++++- plugins/cli/osbuild.py | 9 ++++++ plugins/hub/osbuild.py | 4 +++ test/unit/test_builder.py | 43 +++++++++++++++++++++++++ test/unit/test_cli.py | 66 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) diff --git a/plugins/builder/osbuild.py b/plugins/builder/osbuild.py index a3ca9e1..e6b6922 100644 --- a/plugins/builder/osbuild.py +++ b/plugins/builder/osbuild.py @@ -168,15 +168,19 @@ class ComposeRequest: self.distribution = distro self.image_requests = ireqs self.koji = koji + self.customizations: Optional[dict] = None def as_dict(self): - return { + res = { "distribution": self.distribution, "koji": self.koji.as_dict(), "image_requests": [ img.as_dict() for img in self.image_requests ] } + if self.customizations: + res["customizations"] = self.customizations + return res class ImageStatus(enum.Enum): @@ -641,6 +645,9 @@ class OSBuildImage(BaseTaskHandler): kojidata = ComposeRequest.Koji(self.koji_url, self.id, nvr) request = ComposeRequest(distro, ireqs, kojidata) + # Additional customizations are passed through + request.customizations = opts.get("customizations") + self.upload_json(request.as_dict(), "compose-request") cid = client.compose_create(request) diff --git a/plugins/cli/osbuild.py b/plugins/cli/osbuild.py index e485108..ff167f3 100755 --- a/plugins/cli/osbuild.py +++ b/plugins/cli/osbuild.py @@ -6,6 +6,7 @@ is provided by the koji osbuild plugin for the koji hub. """ +import json import optparse # pylint: disable=deprecated-module from pprint import pprint @@ -46,6 +47,8 @@ def parse_args(argv): parser = kl.OptionParser(usage=kl.get_usage_str(usage)) + parser.add_option("--customizations", type=str, default=None, dest="customizations", + help="Additional customizations to pass to Composer (json file)") parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on image creation") parser.add_option("--ostree-parent", type=str, dest="ostree_parent", @@ -98,6 +101,7 @@ def check_target(session, name): target['dest_tag_name']) +# pylint: disable=too-many-branches @export_cli def handle_osbuild_image(options, session, argv): "[build] Build images via osbuild" @@ -135,6 +139,11 @@ def handle_osbuild_image(options, session, argv): if ostree: opts["ostree"] = ostree + # customizations handling + if args.customizations: + with open(args.customizations, "r", encoding="utf-8") as f: + opts["customizations"] = json.load(f) + # Do some early checks to be able to give quick feedback check_target(session, target) diff --git a/plugins/hub/osbuild.py b/plugins/hub/osbuild.py index 1e954d1..f95d352 100644 --- a/plugins/hub/osbuild.py +++ b/plugins/hub/osbuild.py @@ -87,6 +87,10 @@ OSBUILD_IMAGE_SCHEMA = { "type": "object", "additionalProperties": False, "properties": { + "customizations": { + "type": "object", + "additionalProperties": True + }, "ostree": { "type": "object", "$ref": "#/definitions/ostree" diff --git a/test/unit/test_builder.py b/test/unit/test_builder.py index 06b4aed..adf05e7 100644 --- a/test/unit/test_builder.py +++ b/test/unit/test_builder.py @@ -1026,6 +1026,49 @@ class TestBuilderPlugin(PluginTest): # pylint: disable=too-many-public-methods res = handler.handler(*args) assert res, "invalid compose result" + @httpretty.activate + def test_customizations_compose(self): + # Check we properly handle compose requests with customizations + session = self.mock_session() + handler = self.make_handler(session=session) + + customizations = { + "packages": [ + "emacs" + ] + } + + arches = ["x86_64", "s390x"] + repos = ["http://1.repo", "https://2.repo"] + args = ["name", "version", "distro", + ["image_type"], + "fedora-candidate", + arches, + {"repo": repos, + "customizations": customizations + }] + + url = self.plugin.DEFAULT_COMPOSER_URL + composer = MockComposer(url, architectures=arches) + composer.httpretty_register() + + res = handler.handler(*args) + assert res, "invalid compose result" + compose_id = res["composer"]["id"] + compose = composer.composes.get(compose_id) + self.assertIsNotNone(compose) + + ireqs = compose["request"]["image_requests"] + + # Check we got all the requested architectures + ireq_arches = [i["architecture"] for i in ireqs] + diff = set(arches) ^ set(ireq_arches) + self.assertEqual(diff, set()) + + # Check we actually got the customizations + self.assertEqual(compose["request"].get("customizations"), + customizations) + @httpretty.activate def test_ostree_compose(self): # Check we properly handle ostree compose requests diff --git a/test/unit/test_cli.py b/test/unit/test_cli.py index b713d5b..9366c6b 100644 --- a/test/unit/test_cli.py +++ b/test/unit/test_cli.py @@ -5,6 +5,10 @@ import contextlib import io +import json +import os +import tempfile + import koji import koji_cli.lib as kl from flexmock import flexmock @@ -125,6 +129,68 @@ class TestCliPlugin(PluginTest): r = self.plugin.handle_osbuild_image(options, session, argv) self.assertEqual(r, 0) + def test_customizations_options(self): + with tempfile.TemporaryDirectory() as tmpdir: + + customizations = { + "packages": [ + "emacs" + ] + } + + path = os.path.join(tmpdir, "customizations.json") + + with open(path, "w", encoding="utf-8") as f: + json.dump(customizations, f) + + argv = [ + # the required positional arguments + "name", "version", "distro", "target", "arch1", + # optional keyword arguments + "--repo", "https://first.repo", + "--repo", "https://second.repo", + "--release", "20200202.n2", + "--customizations", path + ] + + expected_args = ["name", "version", "distro", + ['guest-image'], # the default image type + "target", + ['arch1']] + + expected_opts = { + "release": "20200202.n2", + "repo": ["https://first.repo", "https://second.repo"], + "customizations": customizations + } + + task_result = {"compose_id": "42", "build_id": 23} + task_id = 1 + koji_lib = self.mock_koji_lib() + + options = self.mock_options() + session = flexmock() + + self.mock_session_add_valid_tag(session) + + session.should_receive("osbuildImage") \ + .with_args(*expected_args, opts=expected_opts) \ + .and_return(task_id) \ + .once() + + session.should_receive("logout") \ + .with_args() \ + .once() + + session.should_receive("getTaskResult") \ + .with_args(task_id) \ + .and_return(task_result) \ + .once() + + setattr(self.plugin, "kl", koji_lib) + r = self.plugin.handle_osbuild_image(options, session, argv) + self.assertEqual(r, 0) + def test_ostree_options(self): # Check we properly handle ostree specific options