diff --git a/setup.cfg b/setup.cfg index 1f78a9a8..3cb33bac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,6 +77,11 @@ openstack.container.v1 = appcontainer_quota_update = zunclient.osc.v1.quotas:UpdateQuota appcontainer_quota_class_update = zunclient.osc.v1.quota_classes:UpdateQuotaClass appcontainer_quota_class_get = zunclient.osc.v1.quota_classes:GetQuotaClass + appcontainer_registry_create = zunclient.osc.v1.registries:CreateRegistry + appcontainer_registry_list = zunclient.osc.v1.registries:ListRegistry + appcontainer_registry_show = zunclient.osc.v1.registries:ShowRegistry + appcontainer_registry_update = zunclient.osc.v1.registries:UpdateRegistry + appcontainer_registry_delete = zunclient.osc.v1.registries:DeleteRegistry [build_sphinx] source-dir = doc/source diff --git a/zunclient/api_versions.py b/zunclient/api_versions.py index 5266db81..983d4347 100644 --- a/zunclient/api_versions.py +++ b/zunclient/api_versions.py @@ -31,7 +31,7 @@ if not LOG.handlers: HEADER_NAME = "OpenStack-API-Version" SERVICE_TYPE = "container" MIN_API_VERSION = '1.1' -MAX_API_VERSION = '1.29' +MAX_API_VERSION = '1.30' DEFAULT_API_VERSION = '1.latest' _SUBSTITUTIONS = {} diff --git a/zunclient/osc/v1/registries.py b/zunclient/osc/v1/registries.py new file mode 100644 index 00000000..f6269750 --- /dev/null +++ b/zunclient/osc/v1/registries.py @@ -0,0 +1,232 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from osc_lib.command import command +from osc_lib import utils +from oslo_log import log as logging +import six + +from zunclient.common import utils as zun_utils +from zunclient import exceptions as exc +from zunclient.i18n import _ + + +def _get_client(obj, parsed_args): + obj.log.debug("take_action(%s)" % parsed_args) + return obj.app.client_manager.container + + +class CreateRegistry(command.ShowOne): + """Create a registry""" + + log = logging.getLogger(__name__ + ".CreateRegistry") + + def get_parser(self, prog_name): + parser = super(CreateRegistry, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help='The name of the registry.') + parser.add_argument( + '--username', + metavar='', + help='The username to login to the registry.') + parser.add_argument( + '--password', + metavar='', + help='The password to login to the registry.') + parser.add_argument( + '--domain', + metavar='', + required=True, + help='The domain of the registry.') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['name'] = parsed_args.name + opts['domain'] = parsed_args.domain + opts['username'] = parsed_args.username + opts['password'] = parsed_args.password + opts = zun_utils.remove_null_parms(**opts) + registry = client.registries.create(**opts) + return zip(*sorted(six.iteritems(registry._info['registry']))) + + +class ShowRegistry(command.ShowOne): + """Show a registry""" + + log = logging.getLogger(__name__ + ".ShowRegistry") + + def get_parser(self, prog_name): + parser = super(ShowRegistry, self).get_parser(prog_name) + parser.add_argument( + 'registry', + metavar='', + help='ID or name of the registry to show.') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['id'] = parsed_args.registry + opts = zun_utils.remove_null_parms(**opts) + registry = client.registries.get(**opts) + + return zip(*sorted(six.iteritems(registry._info['registry']))) + + +class ListRegistry(command.Lister): + """List available registries""" + + log = logging.getLogger(__name__ + ".ListRegistrys") + + def get_parser(self, prog_name): + parser = super(ListRegistry, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action="store_true", + default=False, + help='List registries in all projects') + parser.add_argument( + '--marker', + metavar='', + help='The last registry UUID of the previous page; ' + 'displays list of registries after "marker".') + parser.add_argument( + '--limit', + metavar='', + type=int, + help='Maximum number of registries to return') + parser.add_argument( + '--sort-key', + metavar='', + help='Column to sort results by') + parser.add_argument( + '--sort-dir', + metavar='', + choices=['desc', 'asc'], + help='Direction to sort. "asc" or "desc".') + parser.add_argument( + '--name', + metavar='', + help='List registries according to their name.') + parser.add_argument( + '--domain', + metavar='', + help='List registries according to their domain.') + parser.add_argument( + '--project-id', + metavar='', + help='List registries according to their project_id') + parser.add_argument( + '--user-id', + metavar='', + help='List registries according to their user_id') + parser.add_argument( + '--username', + metavar='', + help='List registries according to their username') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['all_projects'] = parsed_args.all_projects + opts['marker'] = parsed_args.marker + opts['limit'] = parsed_args.limit + opts['sort_key'] = parsed_args.sort_key + opts['sort_dir'] = parsed_args.sort_dir + opts['domain'] = parsed_args.domain + opts['name'] = parsed_args.name + opts['project_id'] = parsed_args.project_id + opts['user_id'] = parsed_args.user_id + opts['username'] = parsed_args.username + opts = zun_utils.remove_null_parms(**opts) + registries = client.registries.list(**opts) + columns = ('uuid', 'name', 'domain', 'username', 'password') + return (columns, (utils.get_item_properties(registry, columns) + for registry in registries)) + + +class DeleteRegistry(command.Command): + """Delete specified registry(s)""" + + log = logging.getLogger(__name__ + ".Deleteregistry") + + def get_parser(self, prog_name): + parser = super(DeleteRegistry, self).get_parser(prog_name) + parser.add_argument( + 'registry', + metavar='', + nargs='+', + help='ID or name of the registry(s) to delete.') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + registries = parsed_args.registry + for registry in registries: + opts = {} + opts['id'] = registry + opts = zun_utils.remove_null_parms(**opts) + try: + client.registries.delete(**opts) + print(_('Request to delete registry %s has been accepted.') + % registry) + except Exception as e: + print("Delete for registry %(registry)s failed: %(e)s" % + {'registry': registry, 'e': e}) + + +class UpdateRegistry(command.ShowOne): + """Update one or more attributes of the registry""" + log = logging.getLogger(__name__ + ".UpdateRegistry") + + def get_parser(self, prog_name): + parser = super(UpdateRegistry, self).get_parser(prog_name) + parser.add_argument( + 'registry', + metavar='', + help="ID or name of the registry to update.") + parser.add_argument( + '--username', + metavar='', + help='The new username of registry to update.') + parser.add_argument( + '--password', + metavar='', + help='The new password of registry to update.') + parser.add_argument( + '--name', + metavar='', + help='The new name of registry to update.') + parser.add_argument( + '--domain', + metavar='', + help='The new domain of registry to update.') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + registry = parsed_args.registry + opts = {} + opts['username'] = parsed_args.username + opts['password'] = parsed_args.password + opts['domain'] = parsed_args.domain + opts['name'] = parsed_args.name + opts = zun_utils.remove_null_parms(**opts) + if not opts: + raise exc.CommandError("You must update at least one property") + registry = client.registries.update(registry, **opts) + return zip(*sorted(six.iteritems(registry._info['registry']))) diff --git a/zunclient/tests/unit/v1/test_registries.py b/zunclient/tests/unit/v1/test_registries.py new file mode 100644 index 00000000..55e90972 --- /dev/null +++ b/zunclient/tests/unit/v1/test_registries.py @@ -0,0 +1,233 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import testtools +from testtools import matchers +from zunclient import exceptions +from zunclient.tests.unit import utils +from zunclient.v1 import registries + +REGISTRY1 = {'uuid': 'cb9d9848-6c12-411b-a915-6eadc7bd2871', + 'name': 'test1', + 'domain': 'testdomain.io', + 'username': 'testuser1', + 'password': 'testpassword1', + } + +REGISTRY2 = {'uuid': 'c2d923ef-e9b5-4e6b-a8aa-7c54a715b370', + 'name': 'test2', + 'domain': 'otherdomain.io', + 'username': 'testuser2', + 'password': 'testpassword2', + } + + +CREATE_REGISTRY1 = copy.deepcopy(REGISTRY1) +del CREATE_REGISTRY1['uuid'] + +UPDATE_REGISTRY1 = copy.deepcopy(REGISTRY1) +del UPDATE_REGISTRY1['uuid'] +UPDATE_REGISTRY1['name'] = 'newname' + +fake_responses = { + '/v1/registries': + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + 'POST': ( + {}, + {'registry': CREATE_REGISTRY1}, + ), + }, + '/v1/registries/?limit=2': + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/?marker=%s' % REGISTRY2['uuid']: + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/?limit=2&marker=%s' % REGISTRY2['uuid']: + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/?sort_dir=asc': + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/?sort_key=uuid': + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/?sort_key=uuid&sort_dir=desc': + { + 'GET': ( + {}, + {'registries': [REGISTRY1, REGISTRY2]}, + ), + }, + '/v1/registries/%s' % REGISTRY1['uuid']: + { + 'GET': ( + {}, + {'registry': REGISTRY1} + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + {'registry': UPDATE_REGISTRY1}, + ), + }, +} + + +class RegistryManagerTest(testtools.TestCase): + + def setUp(self): + super(RegistryManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = registries.RegistryManager(self.api) + + def test_registry_create(self): + registries = self.mgr.create(**CREATE_REGISTRY1) + expect = [ + ('POST', '/v1/registries', {}, {'registry': CREATE_REGISTRY1}) + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(registries) + + def test_registry_create_fail(self): + create_registry_fail = copy.deepcopy(CREATE_REGISTRY1) + create_registry_fail["wrong_key"] = "wrong" + self.assertRaisesRegex(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(registries.CREATION_ATTRIBUTES)), + self.mgr.create, **create_registry_fail) + self.assertEqual([], self.api.calls) + + def test_registries_list(self): + registries = self.mgr.list() + expect = [ + ('GET', '/v1/registries', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(registries, matchers.HasLength(2)) + + def _test_registries_list_with_filters(self, limit=None, marker=None, + sort_key=None, sort_dir=None, + expect=[]): + registries_filter = self.mgr.list(limit=limit, marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + self.assertEqual(expect, self.api.calls) + self.assertThat(registries_filter, matchers.HasLength(2)) + + def test_registries_list_with_limit(self): + expect = [ + ('GET', '/v1/registries/?limit=2', {}, None), + ] + self._test_registries_list_with_filters( + limit=2, + expect=expect) + + def test_registries_list_with_marker(self): + expect = [ + ('GET', '/v1/registries/?marker=%s' % REGISTRY2['uuid'], + {}, None), + ] + self._test_registries_list_with_filters( + marker=REGISTRY2['uuid'], + expect=expect) + + def test_registries_list_with_marker_limit(self): + expect = [ + ('GET', '/v1/registries/?limit=2&marker=%s' % REGISTRY2['uuid'], + {}, None), + ] + self._test_registries_list_with_filters( + limit=2, marker=REGISTRY2['uuid'], + expect=expect) + + def test_coontainer_list_with_sort_dir(self): + expect = [ + ('GET', '/v1/registries/?sort_dir=asc', {}, None), + ] + self._test_registries_list_with_filters( + sort_dir='asc', + expect=expect) + + def test_registry_list_with_sort_key(self): + expect = [ + ('GET', '/v1/registries/?sort_key=uuid', {}, None), + ] + self._test_registries_list_with_filters( + sort_key='uuid', + expect=expect) + + def test_registry_list_with_sort_key_dir(self): + expect = [ + ('GET', '/v1/registries/?sort_key=uuid&sort_dir=desc', {}, None), + ] + self._test_registries_list_with_filters( + sort_key='uuid', sort_dir='desc', + expect=expect) + + def test_registry_show(self): + registry = self.mgr.get(REGISTRY1['uuid']) + expect = [ + ('GET', '/v1/registries/%s' % REGISTRY1['uuid'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(REGISTRY1['name'], registry._info['registry']['name']) + self.assertEqual(REGISTRY1['uuid'], registry._info['registry']['uuid']) + + def test_registry_update(self): + registry = self.mgr.update(REGISTRY1['uuid'], **UPDATE_REGISTRY1) + expect = [ + ('PATCH', '/v1/registries/%s' % REGISTRY1['uuid'], + {}, + {'registry': UPDATE_REGISTRY1}) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(UPDATE_REGISTRY1['name'], + registry._info['registry']['name']) + + def test_registries_delete(self): + registries = self.mgr.delete(REGISTRY1['uuid']) + expect = [ + ('DELETE', '/v1/registries/%s' % (REGISTRY1['uuid']), + {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(registries) diff --git a/zunclient/v1/client.py b/zunclient/v1/client.py index 8462e5dc..0b4ecee6 100644 --- a/zunclient/v1/client.py +++ b/zunclient/v1/client.py @@ -25,6 +25,7 @@ from zunclient.v1 import hosts from zunclient.v1 import images from zunclient.v1 import quota_classes from zunclient.v1 import quotas +from zunclient.v1 import registries from zunclient.v1 import services from zunclient.v1 import versions @@ -139,6 +140,7 @@ class Client(object): self.actions = actions.ActionManager(self.http_client) self.quotas = quotas.QuotaManager(self.http_client) self.quota_classes = quota_classes.QuotaClassManager(self.http_client) + self.registries = registries.RegistryManager(self.http_client) @property def api_version(self): diff --git a/zunclient/v1/registries.py b/zunclient/v1/registries.py new file mode 100644 index 00000000..95a5cb6f --- /dev/null +++ b/zunclient/v1/registries.py @@ -0,0 +1,102 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from zunclient.common import base +from zunclient.common import utils +from zunclient import exceptions + + +CREATION_ATTRIBUTES = ['name', 'domain', 'username', 'password'] + + +class Registry(base.Resource): + def __repr__(self): + return "" % self._info + + +class RegistryManager(base.Manager): + resource_class = Registry + + @staticmethod + def _path(id=None): + + if id: + return '/v1/registries/%s' % id + else: + return '/v1/registries' + + def list(self, marker=None, limit=None, sort_key=None, + sort_dir=None, all_projects=False, **kwargs): + """Retrieve a list of registries. + + :param all_projects: Optional, list registries in all projects + + :param marker: Optional, the UUID of a registries, eg the last + registries from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of registries to return. + 2) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the ZUN API + (see Zun's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :returns: A list of registries. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, + sort_dir, all_projects) + path = '' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), + "registries", qparams=kwargs) + else: + return self._list_pagination(self._path(path), + "registries", + limit=limit) + + def get(self, id, **kwargs): + try: + return self._list(self._path(id), + qparams=kwargs)[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {'registry': {}} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new['registry'][key] = value + else: + raise exceptions.InvalidAttribute( + "Key must be in %s" % ','.join(CREATION_ATTRIBUTES)) + return self._create(self._path(), new) + + def delete(self, id, **kwargs): + return self._delete(self._path(id), + qparams=kwargs) + + def update(self, id, **patch): + kwargs = {'registry': patch} + return self._update(self._path(id), kwargs) diff --git a/zunclient/v1/registries_shell.py b/zunclient/v1/registries_shell.py new file mode 100644 index 00000000..9996e1d7 --- /dev/null +++ b/zunclient/v1/registries_shell.py @@ -0,0 +1,174 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import yaml + +from zunclient.common import cliutils as utils +from zunclient.common import utils as zun_utils +from zunclient import exceptions as exc + + +def _show_registry(registry): + utils.print_dict(registry._info['registry']) + + +@utils.arg('--name', + metavar='', + help='The name of the registry.') +@utils.arg('--username', + metavar='', + help='The username to login to the registry.') +@utils.arg('--password', + metavar='', + help='The password to login to the registry.') +@utils.arg('--domain', + metavar='', + required=True, + help='The domain of the registry.') +def do_registry_create(cs, args): + """Create a registry.""" + opts = {} + opts['name'] = args.name + opts['domain'] = args.domain + opts['username'] = args.username + opts['password'] = args.password + opts = zun_utils.remove_null_parms(**opts) + _show_registry(cs.registries.create(**opts)) + + +@utils.arg('--all-projects', + action="store_true", + default=False, + help='List registries in all projects') +@utils.arg('--marker', + metavar='', + default=None, + help='The last registry UUID of the previous page; ' + 'displays list of registries after "marker".') +@utils.arg('--limit', + metavar='', + type=int, + help='Maximum number of registries to return') +@utils.arg('--sort-key', + metavar='', + help='Column to sort results by') +@utils.arg('--sort-dir', + metavar='', + choices=['desc', 'asc'], + help='Direction to sort. "asc" or "desc".') +@utils.arg('--name', + metavar='', + help='List registries according to their name.') +@utils.arg('--domain', + metavar='', + help='List registries according to their domain.') +@utils.arg('--project-id', + metavar='', + help='List registries according to their Project_id') +@utils.arg('--user-id', + metavar='', + help='List registries according to their user_id') +@utils.arg('--username', + metavar='', + help='List registries according to their username') +def do_registry_list(cs, args): + """Print a list of available registries.""" + opts = {} + opts['all_projects'] = args.all_projects + opts['marker'] = args.marker + opts['limit'] = args.limit + opts['sort_key'] = args.sort_key + opts['sort_dir'] = args.sort_dir + opts['domain'] = args.domain + opts['name'] = args.name + opts['project_id'] = args.project_id + opts['user_id'] = args.user_id + opts['username'] = args.username + opts = zun_utils.remove_null_parms(**opts) + registries = cs.registries.list(**opts) + columns = ('uuid', 'name', 'domain', 'username', 'password') + utils.print_list(registries, columns, + sortby_index=None) + + +@utils.arg('registries', + metavar='', + nargs='+', + help='ID or name of the (registry)s to delete.') +def do_registry_delete(cs, args): + """Delete specified registries.""" + for registry in args.registries: + opts = {} + opts['id'] = registry + opts = zun_utils.remove_null_parms(**opts) + try: + cs.registries.delete(**opts) + print("Request to delete registry %s has been accepted." % + registry) + except Exception as e: + print("Delete for registry %(registry)s failed: %(e)s" % + {'registry': registry, 'e': e}) + + +@utils.arg('registry', + metavar='', + help='ID or name of the registry to show.') +@utils.arg('-f', '--format', + metavar='', + action='store', + choices=['json', 'yaml', 'table'], + default='table', + help='Print representation of the container.' + 'The choices of the output format is json,table,yaml.' + 'Defaults to table.') +def do_registry_show(cs, args): + """Show details of a registry.""" + opts = {} + opts['id'] = args.registry + opts = zun_utils.remove_null_parms(**opts) + registry = cs.registries.get(**opts) + if args.format == 'json': + print(json.dumps(registry._info, indent=4, sort_keys=True)) + elif args.format == 'yaml': + print(yaml.safe_dump(registry._info, default_flow_style=False)) + elif args.format == 'table': + _show_registry(registry) + + +@utils.arg('registry', + metavar='', + help="ID or name of the registry to update.") +@utils.arg('--username', + metavar='', + help='The username login to the registry.') +@utils.arg('--password', + metavar='', + help='The domain login to the registry.') +@utils.arg('--domain', + metavar='', + help='The domain of the registry.') +@utils.arg('--name', + metavar='', + help='The new name for the registry') +def do_registry_update(cs, args): + """Update one or more attributes of the registry.""" + opts = {} + opts['username'] = args.username + opts['password'] = args.password + opts['domain'] = args.domain + opts['name'] = args.name + opts = zun_utils.remove_null_parms(**opts) + if not opts: + raise exc.CommandError("You must update at least one property") + registry = cs.registries.update(args.registry, **opts) + _show_registry(registry) diff --git a/zunclient/v1/shell.py b/zunclient/v1/shell.py index 73c8c4a9..2c0dd435 100644 --- a/zunclient/v1/shell.py +++ b/zunclient/v1/shell.py @@ -21,6 +21,7 @@ from zunclient.v1 import hosts_shell from zunclient.v1 import images_shell from zunclient.v1 import quota_classes_shell from zunclient.v1 import quotas_shell +from zunclient.v1 import registries_shell from zunclient.v1 import services_shell from zunclient.v1 import versions_shell @@ -35,4 +36,5 @@ COMMAND_MODULES = [ actions_shell, quotas_shell, quota_classes_shell, + registries_shell, ]