diff --git a/doc/configuration.rst b/doc/configuration.rst index 5d9947b1..907f72f4 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -1617,6 +1617,45 @@ OSBuild Composer for building images * ``ostree_ref`` -- name of the ostree branch * ``ostree_parent`` -- commit hash or a a branch-like reference to the parent commit. + * ``upload_options`` -- a dictionary with upload options specific to the + target cloud environment. If provided, the image will be uploaded to the + cloud environment, in addition to the Koji server. One can't combine + arbitrary image types with arbitrary upload options. + The dictionary keys differ based on the target cloud environment. The + following keys are supported: + + * **AWS EC2 upload options** -- upload to Amazon Web Services. + + * ``region`` -- AWS region to upload the image to + * ``share_with_accounts`` -- list of AWS account IDs to share the image + with + * ``snapshot_name`` -- Snapshot name of the uploaded EC2 image + (optional) + + * **AWS S3 upload options** -- upload to Amazon Web Services S3. + + * ``region`` -- AWS region to upload the image to + + * **Azure upload options** -- upload to Microsoft Azure. + + * ``tenant_id`` -- Azure tenant ID to upload the image to + * ``subscription_id`` -- Azure subscription ID to upload the image to + * ``resource_group`` -- Azure resource group to upload the image to + * ``location`` -- Azure location to upload the image to + * ``image_name`` -- Image name of the uploaded Azure image (optional) + + * **GCP upload options** -- upload to Google Cloud Platform. + + * ``region`` -- GCP region to upload the image to + * ``bucket`` -- GCP bucket to upload the image to + * ``share_with_accounts`` -- list of GCP accounts to share the image + with + * ``image_name`` -- Image name of the uploaded GCP image (optional) + + * **Container upload options** -- upload to a container registry. + + * ``name`` -- name of the container image (optional) + * ``tag`` -- container tag to upload the image to (optional) .. note:: There is initial support for having this task as failable without aborting diff --git a/pungi/checks.py b/pungi/checks.py index a014c6be..a7f29606 100644 --- a/pungi/checks.py +++ b/pungi/checks.py @@ -1194,6 +1194,86 @@ def make_schema(): "ostree_url": {"type": "string"}, "ostree_ref": {"type": "string"}, "ostree_parent": {"type": "string"}, + "upload_options": { + "oneOf": [ + # AWSEC2UploadOptions + { + "type": "object", + "additionalProperties": False, + "required": [ + "region", + "share_with_accounts", + ], + "properties": { + "region": { + "type": "string", + }, + "snapshot_name": { + "type": "string", + }, + "share_with_accounts": { + "type": "array", + "items": {"type": "string"}, + }, + }, + }, + # AWSS3UploadOptions + { + "type": "object", + "additionalProperties": False, + "required": ["region"], + "properties": { + "region": {"type": "string"} + }, + }, + # AzureUploadOptions + { + "type": "object", + "additionalProperties": False, + "required": [ + "tenant_id", + "subscription_id", + "resource_group", + "location", + ], + "properties": { + "tenant_id": {"type": "string"}, + "subscription_id": {"type": "string"}, + "resource_group": {"type": "string"}, + "location": {"type": "string"}, + "image_name": { + "type": "string", + }, + }, + }, + # GCPUploadOptions + { + "type": "object", + "additionalProperties": False, + "required": ["region", "bucket"], + "properties": { + "region": {"type": "string"}, + "bucket": {"type": "string"}, + "image_name": { + "type": "string", + }, + "share_with_accounts": { + "type": "array", + "items": {"type": "string"}, + }, + }, + }, + # ContainerUploadOptions + { + "type": "object", + "additionalProperties": False, + "properties": { + "name": {"type": "string"}, + "tag": {"type": "string"}, + }, + }, + ] + }, }, "required": ["name", "distro", "image_types"], "additionalProperties": False, diff --git a/pungi/phases/osbuild.py b/pungi/phases/osbuild.py index 9215acfa..6e52e9c5 100644 --- a/pungi/phases/osbuild.py +++ b/pungi/phases/osbuild.py @@ -126,6 +126,10 @@ class RunOSBuildThread(WorkerThread): if ostree: opts["ostree"] = ostree + upload_options = config.get("upload_options") + if upload_options: + opts["upload_options"] = upload_options + if release: opts["release"] = release task_id = koji.koji_proxy.osbuildImage( diff --git a/tests/test_osbuild_phase.py b/tests/test_osbuild_phase.py index b740a82a..c53f7529 100644 --- a/tests/test_osbuild_phase.py +++ b/tests/test_osbuild_phase.py @@ -399,6 +399,125 @@ class RunOSBuildThreadTest(helpers.PungiTestCase): ], ) + @mock.patch("pungi.util.get_file_size", new=lambda fp: 65536) + @mock.patch("pungi.util.get_mtime", new=lambda fp: 1024) + @mock.patch("pungi.phases.osbuild.Linker") + @mock.patch("pungi.phases.osbuild.kojiwrapper.KojiWrapper") + def test_process_upload_options(self, KojiWrapper, Linker): + cfg = { + "name": "test-image", + "distro": "rhel-8", + "image_types": ["rhel-ec2"], + "upload_options": { + "region": "us-east-1", + "share_with_accounts": ["123456789012"], + }, + } + build_id = 5678 + koji = KojiWrapper.return_value + koji.watch_task.side_effect = self.make_fake_watch(0) + koji.koji_proxy.osbuildImage.return_value = 1234 + koji.koji_proxy.getTaskResult.return_value = { + "composer": {"server": "https://composer.osbuild.org", "id": ""}, + "koji": {"build": build_id}, + } + koji.koji_proxy.getBuild.return_value = { + "build_id": build_id, + "name": "test-image", + "version": "1", + "release": "1", + } + koji.koji_proxy.listArchives.return_value = [ + { + "extra": {"image": {"arch": "x86_64"}}, + "filename": "image.raw.xz", + "type_name": "raw-xz", + } + ] + koji.koji_module.pathinfo = orig_koji.pathinfo + + self.t.process( + ( + self.compose, + self.compose.variants["Everything"], + cfg, + ["x86_64"], + "1", # version + "15", # release + "image-target", + [self.topdir + "/compose/Everything/$arch/os"], + ["x86_64"], + ), + 1, + ) + + # Verify two Koji instances were created. + self.assertEqual(len(KojiWrapper.call_args), 2) + # Verify correct calls to Koji + self.assertEqual( + koji.mock_calls, + [ + mock.call.login(), + mock.call.koji_proxy.osbuildImage( + "test-image", + "1", + "rhel-8", + ["rhel-ec2"], + "image-target", + ["x86_64"], + opts={ + "release": "15", + "repo": [self.topdir + "/compose/Everything/$arch/os"], + "upload_options": { + "region": "us-east-1", + "share_with_accounts": ["123456789012"], + }, + }, + ), + mock.call.save_task_id(1234), + mock.call.watch_task(1234, mock.ANY), + mock.call.koji_proxy.getTaskResult(1234), + mock.call.koji_proxy.getBuild(build_id), + mock.call.koji_proxy.listArchives(buildID=build_id), + ], + ) + + # Assert there is one image added to manifest and the arguments are sane + self.assertEqual( + self.compose.im.add.call_args_list, + [ + mock.call(arch="x86_64", variant="Everything", image=mock.ANY), + ], + ) + for call in self.compose.im.add.call_args_list: + _, kwargs = call + image = kwargs["image"] + self.assertEqual(kwargs["variant"], "Everything") + self.assertIn(kwargs["arch"], ("x86_64")) + self.assertEqual(kwargs["arch"], image.arch) + self.assertEqual( + "Everything/x86_64/images/image.raw.xz", + image.path, + ) + self.assertEqual("raw.xz", image.format) + self.assertEqual("raw-xz", image.type) + self.assertEqual("Everything", image.subvariant) + + self.assertTrue( + os.path.isdir(self.topdir + "/compose/Everything/x86_64/images") + ) + + self.assertEqual( + Linker.return_value.mock_calls, + [ + mock.call.link( + "/mnt/koji/packages/test-image/1/1/images/image.raw.xz", + self.topdir + "/compose/Everything/x86_64/images/image.raw.xz", + link_type="hardlink-or-copy", + ) + ], + ) + @mock.patch("pungi.util.get_file_size", new=lambda fp: 65536) @mock.patch("pungi.util.get_mtime", new=lambda fp: 1024) @mock.patch("pungi.phases.osbuild.Linker")