diff --git a/releasenotes/notes/tripleo-container-image-push-0bff071650976f52.yaml b/releasenotes/notes/tripleo-container-image-push-0bff071650976f52.yaml new file mode 100644 index 000000000..39ea7d7a8 --- /dev/null +++ b/releasenotes/notes/tripleo-container-image-push-0bff071650976f52.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + With the new podman container setup comes an Apache served local image + registry. + + `openstack tripleo container image push` allows you to maintain those + images, and add new images as required. diff --git a/setup.cfg b/setup.cfg index 8c5f9f195..ef2042eca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -107,6 +107,7 @@ openstack.tripleoclient.v1 = tripleo_container_image_delete = tripleoclient.v1.container_image:TripleOContainerImageDelete tripleo_container_image_list = tripleoclient.v1.container_image:TripleOContainerImageList tripleo_container_image_show = tripleoclient.v1.container_image:TripleOContainerImageShow + tripleo_container_image_push = tripleoclient.v1.container_image:TripleOContainerImagePush tripleo_container_image_prepare = tripleoclient.v1.container_image:TripleOImagePrepare tripleo_container_image_prepare_default = tripleoclient.v1.container_image:TripleOImagePrepareDefault undercloud_install = tripleoclient.v1.undercloud:InstallUndercloud diff --git a/tripleoclient/tests/v1/test_container_image.py b/tripleoclient/tests/v1/test_container_image.py index 275aa2be0..c03a9de41 100644 --- a/tripleoclient/tests/v1/test_container_image.py +++ b/tripleoclient/tests/v1/test_container_image.py @@ -24,9 +24,10 @@ import tempfile import yaml from osc_lib import exceptions as oscexc - +from six.moves.urllib import parse from tripleo_common.image import image_uploader from tripleo_common.image import kolla_builder +from tripleoclient import exceptions as tcexc from tripleoclient.tests.v1.test_plugin import TestPluginV1 from tripleoclient.v1 import container_image @@ -82,6 +83,194 @@ class TestContainerImageUpload(TestPluginV1): mock_manager.return_value.upload.assert_called_once_with() +class TestContainerImagePush(TestPluginV1): + def setUp(self): + super(TestContainerImagePush, self).setUp() + self.cmd = container_image.TripleOContainerImagePush(self.app, None) + + @mock.patch('tripleo_common.image.image_uploader.UploadTask') + @mock.patch('tripleo_common.image.image_uploader.ImageUploadManager') + def test_take_action(self, mock_manager, mock_task): + arglist = ['docker.io/namespace/foo'] + verifylist = [('image_to_push', 'docker.io/namespace/foo')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # mock manager object + mock_mgr = mock.Mock() + mock_manager.return_value = mock_mgr + + # mock uploader object + mock_uploader = mock.Mock() + mock_mgr.uploader.return_value = mock_uploader + + # mock return url object from uploader._image_to_url + mock_url = mock.Mock() + container_url = parse.urlparse("docker://docker.io/namespace/foo") + registry_url = parse.urlparse("docker://127.0.0.1:8787") + mock_url.side_effect = [container_url, registry_url] + mock_uploader._image_to_url = mock_url + + # mock return session object from uploader.authenticate + mock_session = mock.Mock() + mock_uploader.authenticate.return_value = mock_session + + # mock upload task + mock_uploadtask = mock.Mock() + mock_task.return_value = mock_uploadtask + + # mock add upload task action + mock_add_upload = mock.Mock() + data = [] + mock_add_upload.return_value = data + mock_uploader.add_upload_task = mock_add_upload + + # mock run tasks action + mock_run_tasks = mock.Mock() + mock_uploader.run_tasks = mock_run_tasks + + self.cmd.take_action(parsed_args) + + mock_task.assert_called_once_with( + image_name='namespace/foo', + pull_source='docker.io', + push_destination=parsed_args.registry_url, + append_tag=parsed_args.append_tag, + modify_role=None, + modify_vars=None, + dry_run=parsed_args.dry_run, + cleanup=False, + multi_arch=parsed_args.multi_arch) + + mock_add_upload.assert_called_once_with(mock_uploadtask) + mock_run_tasks.assert_called_once() + + def test_take_action_local(self): + arglist = ['docker.io/namespace/foo', '--local'] + verifylist = [('image_to_push', 'docker.io/namespace/foo'), + ('local', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(tcexc.NotFound, self.cmd.take_action, parsed_args) + + @mock.patch('tripleo_common.image.image_uploader.UploadTask') + @mock.patch('tripleo_common.image.image_uploader.ImageUploadManager') + def test_take_action_oserror(self, mock_manager, mock_task): + arglist = ['docker.io/namespace/foo'] + verifylist = [('image_to_push', 'docker.io/namespace/foo')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # mock manager object + mock_mgr = mock.Mock() + mock_manager.return_value = mock_mgr + + # mock uploader object + mock_uploader = mock.Mock() + mock_mgr.uploader.return_value = mock_uploader + + # mock return url object from uploader._image_to_url + mock_url = mock.Mock() + container_url = parse.urlparse("docker://docker.io/namespace/foo") + registry_url = parse.urlparse("docker://127.0.0.1:8787") + mock_url.side_effect = [container_url, registry_url] + mock_uploader._image_to_url = mock_url + + # mock return session object from uploader.authenticate + mock_session = mock.Mock() + mock_uploader.authenticate.return_value = mock_session + + # mock upload task + mock_uploadtask = mock.Mock() + mock_task.return_value = mock_uploadtask + + # mock add upload task action + mock_add_upload = mock.Mock() + data = [] + mock_add_upload.return_value = data + mock_uploader.add_upload_task = mock_add_upload + + # mock run tasks action + mock_run_tasks = mock.Mock() + mock_run_tasks.side_effect = OSError('Fail') + mock_uploader.run_tasks = mock_run_tasks + + self.assertRaises(oscexc.CommandError, + self.cmd.take_action, + parsed_args) + + @mock.patch('tripleo_common.image.image_uploader.UploadTask') + @mock.patch('tripleo_common.image.image_uploader.ImageUploadManager') + def test_take_action_all_options(self, mock_manager, mock_task): + arglist = ['--registry-url', '127.0.0.1:8787', + '--append-tag', 'test', + '--username', 'user', + '--password', 'password', + '--dry-run', + '--multi-arch', + '--cleanup', + 'docker.io/namespace/foo:tag'] + verifylist = [('registry_url', '127.0.0.1:8787'), + ('append_tag', 'test'), + ('username', 'user'), + ('password', 'password'), + ('dry_run', True), + ('multi_arch', True), + ('cleanup', True), + ('image_to_push', 'docker.io/namespace/foo:tag')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # mock manager object + mock_mgr = mock.Mock() + mock_manager.return_value = mock_mgr + + # mock uploader object + mock_uploader = mock.Mock() + mock_mgr.uploader.return_value = mock_uploader + + # mock return url object from uploader._image_to_url + mock_url = mock.Mock() + container_url = parse.urlparse("docker://docker.io/namespace/foo:tag") + registry_url = parse.urlparse("docker://127.0.0.1:8787") + mock_url.side_effect = [container_url, registry_url] + mock_uploader._image_to_url = mock_url + + # mock return session object from uploader.authenticate + mock_session = mock.Mock() + mock_uploader.authenticate.return_value = mock_session + + # mock upload task + mock_uploadtask = mock.Mock() + mock_task.return_value = mock_uploadtask + + # mock add upload task action + mock_add_upload = mock.Mock() + data = [] + mock_add_upload.return_value = data + mock_uploader.add_upload_task = mock_add_upload + + # mock run tasks action + mock_run_tasks = mock.Mock() + mock_uploader.run_tasks = mock_run_tasks + + self.cmd.take_action(parsed_args) + + mock_uploader.authenticate.assert_called_once_with( + registry_url, parsed_args.username, parsed_args.password) + + mock_task.assert_called_once_with( + image_name='namespace/foo:tag', + pull_source='docker.io', + push_destination=parsed_args.registry_url, + append_tag=parsed_args.append_tag, + modify_role=None, + modify_vars=None, + dry_run=parsed_args.dry_run, + cleanup=True, + multi_arch=parsed_args.multi_arch) + + mock_add_upload.assert_called_once_with(mock_uploadtask) + mock_run_tasks.assert_called_once() + + class TestContainerImageDelete(TestPluginV1): def setUp(self): diff --git a/tripleoclient/v1/container_image.py b/tripleoclient/v1/container_image.py index 7ed9801c9..97d24948f 100644 --- a/tripleoclient/v1/container_image.py +++ b/tripleoclient/v1/container_image.py @@ -513,6 +513,123 @@ class DiscoverImageTag(command.Command): )) +class TripleOContainerImagePush(command.Command): + """Push specified image to registry.""" + + auth_required = False + log = logging.getLogger(__name__ + ".TripleoContainerImagePush") + + def get_parser(self, prog_name): + parser = super(TripleOContainerImagePush, self).get_parser(prog_name) + parser.add_argument( + "--local", + dest="local", + default=False, + action="store_true", + help=_("Use this flag if the container image is already on the " + "current system and does not need to be pulled from a " + "remote registry.") + ) + parser.add_argument( + "--registry-url", + dest="registry_url", + metavar='', + default=image_uploader.get_undercloud_registry(), + help=_("URL of the destination registry in the form " + ":.") + ) + parser.add_argument( + "--append-tag", + dest="append_tag", + default='', + help=_("Tag to append to the existing tag when pushing the " + "container. ") + ) + parser.add_argument( + "--username", + dest="username", + metavar='', + help=_("Username for the destination image registry.") + ) + parser.add_argument( + "--password", + dest="password", + metavar='', + help=_("Password for the destination image registry.") + ) + parser.add_argument( + "--dry-run", + dest="dry_run", + action="store_true", + help=_("Perform a dry run upload. The upload action is not " + "performed, but the authentication process is attempted.") + ) + parser.add_argument( + "--multi-arch", + dest="multi_arch", + action="store_true", + help=_("Enable multi arch support for the upload.") + ) + parser.add_argument( + "--cleanup", + dest="cleanup", + action="store_true", + default=False, + help=_("Remove local copy of the image after uploading") + ) + parser.add_argument( + dest="image_to_push", + metavar='', + help=_("Container image to upload. Should be in the form of " + "//:. If tag is " + "not provided, then latest will be used.") + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + # TODO(aschultz): need to fix upload to be able to handle local source + if parsed_args.local: + raise exceptions.NotFound('--local is currently not implemented') + + manager = image_uploader.ImageUploadManager() + uploader = manager.uploader('python') + + source_url = uploader._image_to_url(parsed_args.image_to_push) + image_name = source_url.path[1:] + if len(image_name.split('/')) != 2: + raise exceptions.DownloadError('Invalid container. Provided ' + 'container image should be ' + '//:' + '') + image_source = source_url.netloc + + reg_url = uploader._image_to_url(parsed_args.registry_url) + + uploader.authenticate(reg_url, + parsed_args.username, + parsed_args.password) + + task = image_uploader.UploadTask( + image_name=image_name, + pull_source=image_source, + push_destination=parsed_args.registry_url, + append_tag=parsed_args.append_tag, + modify_role=None, + modify_vars=None, + dry_run=parsed_args.dry_run, + cleanup=parsed_args.cleanup, + multi_arch=parsed_args.multi_arch) + try: + uploader.add_upload_task(task) + uploader.run_tasks() + except OSError as e: + self.log.error("Unable to upload due to permissions. " + "Please prefix command with sudo.") + raise oscexc.CommandError(e) + + class TripleOContainerImageDelete(command.Command): """Delete specified image from registry."""