From b0d2d4924cf26ca02ab2265eafd06fb418407bb1 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Fri, 24 Oct 2014 09:03:22 -0700 Subject: [PATCH 1/6] Expose waiters in the CLI Add a ``wait`` command to all services that have waiters. For each type of waiter, a subcommand representing that waiter was added. For example, to wait for an ec2 instance to reach the running state, the wait command would be specified as ``aws ec2 wait instance-running``. --- awscli/clidocs.py | 11 +- awscli/clidriver.py | 3 +- awscli/customizations/waiters.py | 216 ++++++++++++ awscli/handlers.py | 2 + doc/source/htmlgen | 17 +- .../customizations/test_waiters.py | 44 +++ tests/unit/customizations/test_waiters.py | 319 ++++++++++++++++++ tests/unit/test_completer.py | 2 +- 8 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 awscli/customizations/waiters.py create mode 100644 tests/integration/customizations/test_waiters.py create mode 100644 tests/unit/customizations/test_waiters.py diff --git a/awscli/clidocs.py b/awscli/clidocs.py index a9e44741fa02..833d7aba3800 100644 --- a/awscli/clidocs.py +++ b/awscli/clidocs.py @@ -232,7 +232,16 @@ def doc_subitems_start(self, help_command, **kwargs): def doc_subitem(self, command_name, help_command, **kwargs): doc = help_command.doc - doc.style.tocitem(command_name) + subcommand = help_command.command_table[command_name] + subcommand_table = getattr(subcommand, 'subcommand_table', {}) + # If the subcommand table has commands in it, + # direct the subitem to the command's index because + # it has more subcommands to be documented. + if (len(subcommand_table) > 0): + file_name = '%s/index' % command_name + doc.style.tocitem(command_name, file_name=file_name) + else: + doc.style.tocitem(command_name) class OperationDocumentEventHandler(CLIDocumentEventHandler): diff --git a/awscli/clidriver.py b/awscli/clidriver.py index cc559486995f..5567c68c8401 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -366,7 +366,8 @@ def _create_command_table(self): service_object=service_object) self.session.emit('building-command-table.%s' % self._name, command_table=command_table, - session=self.session) + session=self.session, + service_object=service_object) return command_table def create_help_command(self): diff --git a/awscli/customizations/waiters.py b/awscli/customizations/waiters.py new file mode 100644 index 000000000000..f292af2d403a --- /dev/null +++ b/awscli/customizations/waiters.py @@ -0,0 +1,216 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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 botocore import xform_name + +from awscli.clidriver import ServiceOperation +from awscli.customizations.commands import BasicCommand, BasicHelp, \ + BasicDocHandler + + +def register_add_waiters(cli): + cli.register('building-command-table', add_waiters) + + +def add_waiters(command_table, session, service_object=None, **kwargs): + # If a service object was passed in, try to add a wait command. + if service_object is not None: + # Get a client out of the service object. + client = translate_service_object_to_client(service_object) + # Find all of the waiters for that client. + waiters = client.waiter_names + # If there are waiters make a wait command. + if waiters: + command_table['wait'] = WaitCommand(client, service_object) + + +def translate_service_object_to_client(service_object): + # Create a client from a service object. + session = service_object.session + return session.create_client(service_object.service_name) + + +class WaitCommand(BasicCommand): + NAME = 'wait' + DESCRIPTION = 'Wait until a particular condition is satisfied.' + + def __init__(self, client, service_object): + self._client = client + self._service_object = service_object + self.waiter_cmd_builder = WaiterStateCommandBuilder( + client=self._client, + service_object=self._service_object + ) + super(WaitCommand, self).__init__(self._service_object.session) + + def _run_main(self, parsed_args, parsed_globals): + if parsed_args.subcommand is None: + raise ValueError("usage: aws [options] " + "[parameters]\naws: error: too few arguments") + + def _build_subcommand_table(self): + subcommand_table = super(WaitCommand, self)._build_subcommand_table() + self.waiter_cmd_builder.build_all_waiter_state_cmds(subcommand_table) + return subcommand_table + + def create_help_command(self): + return BasicHelp(self._session, self, + command_table=self.subcommand_table, + arg_table=self.arg_table, + event_handler_class=WaiterCommandDocHandler) + + +class WaiterStateCommandBuilder(object): + def __init__(self, client, service_object): + self._client = client + self._service_object = service_object + + def build_all_waiter_state_cmds(self, subcommand_table): + """This adds waiter state commands to the subcommand table passed in. + + This is the method that adds waiter state commands like + ``instance-running`` to ``ec2 wait``. + """ + waiters = self._client.waiter_names + for waiter_name in waiters: + waiter_cli_name = waiter_name.replace('_', '-') + subcommand_table[waiter_cli_name] = \ + self._build_waiter_state_cmd(waiter_name) + + def _build_waiter_state_cmd(self, waiter_name): + # Get the waiter + waiter = self._client.get_waiter(waiter_name) + + # Create the cli name for the waiter operation + waiter_cli_name = waiter_name.replace('_', '-') + + # Obtain the name of the service operation that is used to implement + # the specified waiter. + operation_name = waiter.config.operation + + # Create an operation object to make a command for the waiter. The + # operation object is used to generate the arguments for the waiter + # state command. + operation_object = self._service_object.get_operation(operation_name) + waiter_state_command = WaiterStateCommand( + name=waiter_cli_name, parent_name='wait', + operation_object=operation_object, + operation_caller=WaiterCaller(self._client, waiter), + service_object=self._service_object + ) + # Build the top level description for the waiter state command. + # Most waiters do not have a description so they need to be generated + # using the waiter configuration. + waiter_state_doc_builder = WaiterStateDocBuilder(waiter.config) + description = waiter_state_doc_builder.build_waiter_state_description() + waiter_state_command.DESCRIPTION = description + return waiter_state_command + + +class WaiterStateDocBuilder(object): + SUCCESS_DESCRIPTIONS = { + 'error': u'%s is thrown ', + 'path': u'%s ', + 'pathAll': u'%s for all elements ', + 'pathAny': u'%s for any element ', + 'status': u'%s response is received ' + } + + def __init__(self, waiter_config): + self._waiter_config = waiter_config + + def build_waiter_state_description(self): + description = self._waiter_config.description + # Use the description provided in the waiter config file. If no + # description is provided, use a heuristic to generate a description + # for the waiter. + if not description: + description = u'Wait until ' + # Look at all of the acceptors and find the success state + # acceptor. + for acceptor in self._waiter_config.acceptors: + # Build the description off of the success acceptor. + if acceptor.state == 'success': + description += self._build_success_description(acceptor) + break + # Include what operation is being used. + description += self._build_operation_description( + self._waiter_config.operation) + return description + + def _build_success_description(self, acceptor): + matcher = acceptor.matcher + # Pick the description template to use based on what the matcher is. + success_description = self.SUCCESS_DESCRIPTIONS[matcher] + resource_description = None + # If success is based off of the state of a resource include the + # description about what resource is looked at. + if matcher in ['path', 'pathAny', 'pathAll']: + resource_description = u'JMESPath query %s returns ' % \ + acceptor.argument + # Prepend the resource description to the template description + success_description = resource_description + success_description + # Complete the description by filling in the expected success state. + full_success_description = success_description % acceptor.expected + return full_success_description + + def _build_operation_description(self, operation): + operation_name = xform_name(operation).replace('_', '-') + return u'when polling with ``%s``.' % operation_name + + +class WaiterCaller(object): + def __init__(self, client, waiter): + self._client = client + self._waiter = waiter + + def invoke(self, operation_object, parameters, parsed_globals): + # Create the endpoint based on the parsed globals + endpoint = operation_object.service.get_endpoint( + region_name=parsed_globals.region, + endpoint_url=parsed_globals.endpoint_url, + verify=parsed_globals.verify_ssl) + # Change the client's endpoint using the newly configured endpoint + self._client._endpoint = endpoint + # Call the waiter's wait method. + self._waiter.wait(**parameters) + return 0 + + +class WaiterStateCommand(ServiceOperation): + DESCRIPTION = '' + + def create_help_command(self): + help_command = super(WaiterStateCommand, self).create_help_command() + # Change the operation object's description by changing it to the + # description for a waiter state command. + self._operation_object.documentation = self.DESCRIPTION + # Change the output shape because waiters provide no output. + self._operation_object.model.output_shape = None + return help_command + + +class WaiterCommandDocHandler(BasicDocHandler): + def doc_synopsis_start(self, help_command, **kwargs): + pass + + def doc_synopsis_option(self, arg_name, help_command, **kwargs): + pass + + def doc_synopsis_end(self, help_command, **kwargs): + pass + + def doc_options_start(self, help_command, **kwargs): + pass + + def doc_option(self, arg_name, help_command, **kwargs): + pass diff --git a/awscli/handlers.py b/awscli/handlers.py index e9d2ce941f60..d7493ee46bfc 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -51,6 +51,7 @@ from awscli.customizations.cliinputjson import register_cli_input_json from awscli.customizations.generatecliskeleton import \ register_generate_cli_skeleton +from awscli.customizations.waiters import register_add_waiters def awscli_initialize(event_handlers): @@ -105,3 +106,4 @@ def awscli_initialize(event_handlers): register_cloudsearchdomain(event_handlers) register_s3_endpoint(event_handlers) register_generate_cli_skeleton(event_handlers) + register_add_waiters(event_handlers) diff --git a/doc/source/htmlgen b/doc/source/htmlgen index 36e5a2461e5f..57593c764ad4 100755 --- a/doc/source/htmlgen +++ b/doc/source/htmlgen @@ -33,8 +33,10 @@ def do_operation(driver, service_path, operation_name, operation_command): help_command(None, None) -def do_service(driver, ref_path, service_name, service_command): - print('...%s' % service_name) +def do_service(driver, ref_path, service_name, service_command, + is_top_level_service=True): + if is_top_level_service: + print('...%s' % service_name) service_path = os.path.join(ref_path, service_name) if not os.path.isdir(service_path): os.mkdir(service_path) @@ -50,7 +52,16 @@ def do_service(driver, ref_path, service_name, service_command): if operation_name == 'help': continue operation_command = help_command.command_table[operation_name] - do_operation(driver, service_path, operation_name, operation_command) + subcommand_table = getattr(operation_command, 'subcommand_table', {}) + # If the operation command has a subcommand table with commands + # in it, treat it as a service command as opposed to an operation + # command. + if (len(subcommand_table) > 0): + do_service(driver, service_path, operation_name, + operation_command, False) + else: + do_operation(driver, service_path, operation_name, + operation_command) def do_provider(driver): diff --git a/tests/integration/customizations/test_waiters.py b/tests/integration/customizations/test_waiters.py new file mode 100644 index 000000000000..b09ca746fe4c --- /dev/null +++ b/tests/integration/customizations/test_waiters.py @@ -0,0 +1,44 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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 botocore.session +import random + +from awscli.testutils import unittest, aws + + +class TestDynamoDBWait(unittest.TestCase): + def setUp(self): + self.session = botocore.session.get_session() + self.client = self.session.create_client('dynamodb', 'us-west-2') + + def test_wait_table_exists(self): + # Create a table. + table_name = 'awscliddb-%s' % random.randint(1, 10000) + self.client.create_table( + TableName=table_name, + ProvisionedThroughput={"ReadCapacityUnits": 5, + "WriteCapacityUnits": 5}, + KeySchema=[{"AttributeName": "foo", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "foo", + "AttributeType": "S"}]) + self.addCleanup(self.client.delete_table, TableName=table_name) + + # Wait for the table to be active. + p = aws( + 'dynamodb wait table-exists --table-name %s --region us-west-2' % + table_name) + self.assertEqual(p.rc, 0) + + # Make sure the table is active. + parsed = self.client.describe_table(TableName=table_name) + self.assertEqual(parsed['Table']['TableStatus'], 'ACTIVE') diff --git a/tests/unit/customizations/test_waiters.py b/tests/unit/customizations/test_waiters.py new file mode 100644 index 000000000000..3de876c02f14 --- /dev/null +++ b/tests/unit/customizations/test_waiters.py @@ -0,0 +1,319 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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 mock + +from awscli.testutils import unittest, BaseAWSHelpOutputTest, \ + BaseAWSCommandParamsTest +from awscli.customizations.waiters import add_waiters, WaitCommand, \ + translate_service_object_to_client, WaiterStateCommand, WaiterCaller, \ + WaiterStateDocBuilder, WaiterStateCommandBuilder + + +class TestAddWaiters(unittest.TestCase): + def setUp(self): + self.service_object = mock.Mock() + self.session = mock.Mock() + self.client = mock.Mock() + + # Set up the mock service object. + self.service_object.session = self.session + + # Set up the mock session. + self.session.create_client.return_value = self.client + + # Set up the mock client. + self.client.waiter_names = ['waiter'] + + def test_add_waiters(self): + command_table = {} + add_waiters(command_table, self.session, self.service_object) + # Make sure a wait command was added. + self.assertIn('wait', command_table) + self.assertIsInstance(command_table['wait'], WaitCommand) + + def test_add_waiters_no_waiter_names(self): + self.client.waiter_names = [] + command_table = {} + add_waiters(command_table, self.session, self.service_object) + # Make sure that no wait command was added since the service object + # has no waiters. + self.assertEqual(command_table, {}) + + def test_add_waiters_no_service_object(self): + command_table = {} + add_waiters(command_table, self.session, None) + # Make sure that no wait command was added since no service object + # was passed in. + self.assertEqual(command_table, {}) + + +class TestTranslateServiceObjectToClient(unittest.TestCase): + def test_translate_service_object_to_client(self): + service_object = mock.Mock() + session = mock.Mock() + service_object.session = session + service_object.service_name = 'service' + translate_service_object_to_client(service_object) + session.create_client.assert_called_with('service') + + +class TestWaitCommand(unittest.TestCase): + def setUp(self): + self.client = mock.Mock() + self.service_object = mock.Mock() + self.cmd = WaitCommand(self.client, self.service_object) + + def test_run_main_error(self): + self.parsed_args = mock.Mock() + self.parsed_args.subcommand = None + with self.assertRaises(ValueError): + self.cmd._run_main(self.parsed_args, None) + + +class TestWaitHelpOutput(BaseAWSHelpOutputTest): + def test_wait_command_is_in_list(self): + self.driver.main(['ec2', 'help']) + self.assert_contains('* wait') + + def test_wait_help_command(self): + self.driver.main(['ec2', 'wait', 'help']) + self.assert_contains('Wait until a particular condition is satisfied.') + self.assert_contains('* instance-running') + self.assert_contains('* vpc-available') + + def test_wait_state_help_command(self): + self.driver.main(['ec2', 'wait', 'instance-running', 'help']) + self.assert_contains('``describe-instances``') + self.assert_contains('[--filters ]') + self.assert_contains('``--filters`` (list)') + + +class TestWait(BaseAWSCommandParamsTest): + """ This is merely a smoke test. + + Its purpose is to test that the wait command can be run proberly for + various services. It is by no means exhaustive. + """ + def test_ec2_instance_running(self): + cmdline = 'ec2 wait instance-running' + cmdline += ' --instance-ids i-12345678 i-87654321' + cmdline += """ --filters {"Name":"group-name","Values":["foobar"]}""" + result = {'Filter.1.Value.1': 'foobar', 'Filter.1.Name': 'group-name', + 'InstanceId.1': 'i-12345678', 'InstanceId.2': 'i-87654321'} + self.parsed_response = { + 'Reservations': [{ + 'Instances': [{ + 'State': { + 'Name': 'running' + } + }] + }] + } + self.assert_params_for_cmd(cmdline, result) + + def test_dynamodb_table_exists(self): + cmdline = 'dynamodb wait table-exists' + cmdline += ' --table-name mytable' + result = '{"TableName": "mytable"}' + self.parsed_response = {'Table': {'TableStatus': 'ACTIVE'}} + self.assert_params_for_cmd(cmdline, result) + + def test_elastictranscoder_jobs_complete(self): + cmdline = 'rds wait db-instance-available' + cmdline += ' --db-instance-identifier abc' + result = {'DBInstanceIdentifier': 'abc'} + self.parsed_response = { + 'DBInstances': [{ + 'DBInstanceStatus': 'available' + }] + } + self.assert_params_for_cmd(cmdline, result) + + +class TestWaiterStateCommandBuilder(unittest.TestCase): + def setUp(self): + self.client = mock.Mock() + self.service_object = mock.Mock() + + # Create some waiters. + self.client.waiter_names = ['instance_running', 'bucket_exists'] + self.instance_running_waiter = mock.Mock() + self.bucket_exists_waiter = mock.Mock() + + # Make a mock waiter config. + self.waiter_config = mock.Mock() + self.waiter_config.operation = 'MyOperation' + self.waiter_config.description = 'my waiter description' + self.instance_running_waiter.config = self.waiter_config + self.bucket_exists_waiter.config = self.waiter_config + + self.client.get_waiter.side_effect = [ + self.instance_running_waiter, self.bucket_exists_waiter] + + self.waiter_builder = WaiterStateCommandBuilder( + self.client, + self.service_object + ) + + def test_build_waiter_state_cmds(self): + subcommand_table = {} + self.waiter_builder.build_all_waiter_state_cmds(subcommand_table) + # Check the commands are in the command table + self.assertEqual(len(subcommand_table), 2) + self.assertIn('instance-running', subcommand_table) + self.assertIn('bucket-exists', subcommand_table) + + # Make sure that the correct operation object was used. + self.service_object.get_operation.assert_called_with('MyOperation') + + # Introspect the commands in the command table + instance_running_cmd = subcommand_table['instance-running'] + bucket_exists_cmd = subcommand_table['bucket-exists'] + + # Check that the instance type is correct. + self.assertIsInstance(instance_running_cmd, WaiterStateCommand) + self.assertIsInstance(bucket_exists_cmd, WaiterStateCommand) + + # Check the descriptions are set correctly. + self.assertEqual( + instance_running_cmd.DESCRIPTION, + self.waiter_config.description + ) + self.assertEqual( + bucket_exists_cmd.DESCRIPTION, + self.waiter_config.description + ) + + +class TestWaiterStateDocBuilder(unittest.TestCase): + def setUp(self): + self.waiter_config = mock.Mock() + self.waiter_config.description = '' + self.waiter_config.operation = 'MyOperation' + + # Set up the acceptors. + self.success_acceptor = mock.Mock() + self.success_acceptor.state = 'success' + self.fail_acceptor = mock.Mock() + self.fail_acceptor.state = 'failure' + self.error_acceptor = mock.Mock() + self.error_acceptor.state = 'error' + self.waiter_config.acceptors = [ + self.fail_acceptor, + self.success_acceptor, + self.error_acceptor + ] + + self.doc_builder = WaiterStateDocBuilder(self.waiter_config) + + def test_config_provided_description(self): + # Description is provided by the config file + self.waiter_config.description = 'my description' + description = self.doc_builder.build_waiter_state_description() + self.assertEqual(description, 'my description') + + def test_error_acceptor(self): + self.success_acceptor.matcher = 'error' + self.success_acceptor.expected = 'MyException' + description = self.doc_builder.build_waiter_state_description() + self.assertEqual( + description, + 'Wait until MyException is thrown when polling with ' + '``my-operation``.' + ) + + def test_status_acceptor(self): + self.success_acceptor.matcher = 'status' + self.success_acceptor.expected = 200 + description = self.doc_builder.build_waiter_state_description() + self.assertEqual( + description, + 'Wait until 200 response is received when polling with ' + '``my-operation``.' + ) + + def test_path_acceptor(self): + self.success_acceptor.matcher = 'path' + self.success_acceptor.argument = 'MyResource.name' + self.success_acceptor.expected = 'running' + description = self.doc_builder.build_waiter_state_description() + self.assertEqual( + description, + 'Wait until JMESPath query MyResource.name returns running when ' + 'polling with ``my-operation``.' + ) + + def test_path_all_acceptor(self): + self.success_acceptor.matcher = 'pathAll' + self.success_acceptor.argument = 'MyResource[].name' + self.success_acceptor.expected = 'running' + description = self.doc_builder.build_waiter_state_description() + self.assertEqual( + description, + 'Wait until JMESPath query MyResource[].name returns running for ' + 'all elements when polling with ``my-operation``.' + ) + + def test_path_any_acceptor(self): + self.success_acceptor.matcher = 'pathAny' + self.success_acceptor.argument = 'MyResource[].name' + self.success_acceptor.expected = 'running' + description = self.doc_builder.build_waiter_state_description() + self.assertEqual( + description, + 'Wait until JMESPath query MyResource[].name returns running for ' + 'any element when polling with ``my-operation``.' + ) + + +class TestWaiterCaller(unittest.TestCase): + def test_invoke(self): + client = mock.Mock() + waiter = mock.Mock() + operation_object = mock.Mock() + + parameters = {'Foo': 'bar', 'Baz': 'biz'} + parsed_globals = mock.Mock() + parsed_globals.region = 'us-east-1' + parsed_globals.endpoint_url = 'myurl' + parsed_globals.verify_ssl = True + + waiter_caller = WaiterCaller(client, waiter) + waiter_caller.invoke(operation_object, parameters, parsed_globals) + # Make sure the endpoint was created properly + operation_object.service.get_endpoint.assert_called_with( + region_name=parsed_globals.region, + endpoint_url=parsed_globals.endpoint_url, + verify=parsed_globals.verify_ssl + ) + # Ensure the wait command was called properly. + waiter.wait.assert_called_with( + Foo='bar', Baz='biz') + + +class TestWaiterStateCommand(unittest.TestCase): + def test_create_help_command(self): + operation_object = mock.Mock() + operation_object.model.input_shape = None + cmd = WaiterStateCommand( + name='wait-state', parent_name='wait', + operation_object=operation_object, + operation_caller=mock.Mock(), + service_object=mock.Mock() + ) + cmd.DESCRIPTION = 'mydescription' + cmd.create_help_command() + # Make sure that the description is used and output shape is set + # to None for creating the help command. + self.assertEqual(operation_object.documentation, 'mydescription') + self.assertIsNone(operation_object.model.output_shape) diff --git a/tests/unit/test_completer.py b/tests/unit/test_completer.py index 6ef516494ef1..3c22b837e737 100644 --- a/tests/unit/test_completer.py +++ b/tests/unit/test_completer.py @@ -98,7 +98,7 @@ 'modify-cluster-attributes', 'modify-instance-groups', 'put', 'remove-tags', 'restore-from-hbase-backup', 'schedule-hbase-backup', 'socks', 'ssh', - 'terminate-clusters'])) + 'terminate-clusters', 'wait'])) ] From b9996df92559bf2c0f136b5cb9c57b4faab52131 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Wed, 5 Nov 2014 16:06:16 -0800 Subject: [PATCH 2/6] Update waiter code based on feedback --- awscli/clidriver.py | 9 +++++++-- awscli/customizations/commands.py | 3 ++- awscli/customizations/waiters.py | 21 ++++++++++++--------- tests/unit/customizations/test_waiters.py | 23 +++++++++++++++++++---- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 5567c68c8401..0b9eaaeb404f 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -117,7 +117,8 @@ def _build_command_table(self): command_table = self._build_builtin_commands(self.session) self.session.emit('building-command-table.main', command_table=command_table, - session=self.session) + session=self.session, + command_object=self) return command_table def _build_builtin_commands(self, session): @@ -334,6 +335,10 @@ def name(self): def name(self, value): self._name = value + @property + def service_object(self): + return self._service_object + def _get_command_table(self): if self._command_table is None: self._command_table = self._create_command_table() @@ -367,7 +372,7 @@ def _create_command_table(self): self.session.emit('building-command-table.%s' % self._name, command_table=command_table, session=self.session, - service_object=service_object) + command_object=self) return command_table def create_help_command(self): diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py index 0e1a23750daa..37381960bc7f 100644 --- a/awscli/customizations/commands.py +++ b/awscli/customizations/commands.py @@ -211,7 +211,8 @@ def _build_subcommand_table(self): subcommand_table[subcommand_name] = subcommand_class(self._session) self._session.emit('building-command-table.%s' % self.NAME, command_table=subcommand_table, - session=self._session) + session=self._session, + command_object=self) return subcommand_table def _display_help(self, parsed_args, parsed_globals): diff --git a/awscli/customizations/waiters.py b/awscli/customizations/waiters.py index f292af2d403a..c54b45f5fe1c 100644 --- a/awscli/customizations/waiters.py +++ b/awscli/customizations/waiters.py @@ -21,8 +21,11 @@ def register_add_waiters(cli): cli.register('building-command-table', add_waiters) -def add_waiters(command_table, session, service_object=None, **kwargs): - # If a service object was passed in, try to add a wait command. +def add_waiters(command_table, session, command_object, **kwargs): + # Check if the command object passed in has a ``service_object``. We + # only want to add wait commands to top level model-driven services. + # These require service objects. + service_object = getattr(command_object, 'service_object', None) if service_object is not None: # Get a client out of the service object. client = translate_service_object_to_client(service_object) @@ -104,7 +107,7 @@ def _build_waiter_state_cmd(self, waiter_name): waiter_state_command = WaiterStateCommand( name=waiter_cli_name, parent_name='wait', operation_object=operation_object, - operation_caller=WaiterCaller(self._client, waiter), + operation_caller=WaiterCaller(self._client, waiter_name), service_object=self._service_object ) # Build the top level description for the waiter state command. @@ -169,9 +172,9 @@ def _build_operation_description(self, operation): class WaiterCaller(object): - def __init__(self, client, waiter): + def __init__(self, client, waiter_name): self._client = client - self._waiter = waiter + self._waiter_name = waiter_name def invoke(self, operation_object, parameters, parsed_globals): # Create the endpoint based on the parsed globals @@ -179,10 +182,10 @@ def invoke(self, operation_object, parameters, parsed_globals): region_name=parsed_globals.region, endpoint_url=parsed_globals.endpoint_url, verify=parsed_globals.verify_ssl) - # Change the client's endpoint using the newly configured endpoint - self._client._endpoint = endpoint - # Call the waiter's wait method. - self._waiter.wait(**parameters) + # Make a clone of the client using the newly configured endpoint + client = self._client.clone_client(endpoint=endpoint) + # Make the waiter and call its wait method. + client.get_waiter(self._waiter_name).wait(**parameters) return 0 diff --git a/tests/unit/customizations/test_waiters.py b/tests/unit/customizations/test_waiters.py index 3de876c02f14..73a6dea63497 100644 --- a/tests/unit/customizations/test_waiters.py +++ b/tests/unit/customizations/test_waiters.py @@ -25,6 +25,9 @@ def setUp(self): self.session = mock.Mock() self.client = mock.Mock() + self.command_object = mock.Mock() + self.command_object.service_object = self.service_object + # Set up the mock service object. self.service_object.session = self.session @@ -36,7 +39,7 @@ def setUp(self): def test_add_waiters(self): command_table = {} - add_waiters(command_table, self.session, self.service_object) + add_waiters(command_table, self.session, self.command_object) # Make sure a wait command was added. self.assertIn('wait', command_table) self.assertIsInstance(command_table['wait'], WaitCommand) @@ -44,14 +47,15 @@ def test_add_waiters(self): def test_add_waiters_no_waiter_names(self): self.client.waiter_names = [] command_table = {} - add_waiters(command_table, self.session, self.service_object) + add_waiters(command_table, self.session, self.command_object) # Make sure that no wait command was added since the service object # has no waiters. self.assertEqual(command_table, {}) def test_add_waiters_no_service_object(self): command_table = {} - add_waiters(command_table, self.session, None) + self.command_object.service_object = None + add_waiters(command_table, self.session, self.command_object) # Make sure that no wait command was added since no service object # was passed in. self.assertEqual(command_table, {}) @@ -280,15 +284,21 @@ class TestWaiterCaller(unittest.TestCase): def test_invoke(self): client = mock.Mock() waiter = mock.Mock() + waiter_name = 'my_waiter' operation_object = mock.Mock() + # Mock the clone of the client + cloned_client = mock.Mock() + cloned_client.get_waiter.return_value = waiter + client.clone_client.return_value = cloned_client + parameters = {'Foo': 'bar', 'Baz': 'biz'} parsed_globals = mock.Mock() parsed_globals.region = 'us-east-1' parsed_globals.endpoint_url = 'myurl' parsed_globals.verify_ssl = True - waiter_caller = WaiterCaller(client, waiter) + waiter_caller = WaiterCaller(client, waiter_name) waiter_caller.invoke(operation_object, parameters, parsed_globals) # Make sure the endpoint was created properly operation_object.service.get_endpoint.assert_called_with( @@ -296,6 +306,11 @@ def test_invoke(self): endpoint_url=parsed_globals.endpoint_url, verify=parsed_globals.verify_ssl ) + # Ensure the client was cloned with using the new endpoint. + clone_kwargs = client.clone_client.call_args[1] + self.assertIn('endpoint', clone_kwargs) + # Ensure we get the waiter. + cloned_client.get_waiter.assert_called_with(waiter_name) # Ensure the wait command was called properly. waiter.wait.assert_called_with( Foo='bar', Baz='biz') From f8e5fea63873007b6a8235dcd664209ed8b16dab Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Thu, 6 Nov 2014 13:45:35 -0800 Subject: [PATCH 3/6] Revert "Merge pull request #985 from kyleknap/waiters" This reverts commit 431b6949e68686378226796cace1cfc866f82613, reversing changes made to 0a5605e3bcf9372b9f92b3b6eae684df7b16b2eb. --- awscli/clidocs.py | 11 +- awscli/clidriver.py | 10 +- awscli/customizations/commands.py | 3 +- awscli/customizations/waiters.py | 219 ------------ awscli/handlers.py | 2 - doc/source/htmlgen | 17 +- .../customizations/test_waiters.py | 44 --- tests/unit/customizations/test_waiters.py | 334 ------------------ tests/unit/test_completer.py | 2 +- 9 files changed, 8 insertions(+), 634 deletions(-) delete mode 100644 awscli/customizations/waiters.py delete mode 100644 tests/integration/customizations/test_waiters.py delete mode 100644 tests/unit/customizations/test_waiters.py diff --git a/awscli/clidocs.py b/awscli/clidocs.py index 833d7aba3800..a9e44741fa02 100644 --- a/awscli/clidocs.py +++ b/awscli/clidocs.py @@ -232,16 +232,7 @@ def doc_subitems_start(self, help_command, **kwargs): def doc_subitem(self, command_name, help_command, **kwargs): doc = help_command.doc - subcommand = help_command.command_table[command_name] - subcommand_table = getattr(subcommand, 'subcommand_table', {}) - # If the subcommand table has commands in it, - # direct the subitem to the command's index because - # it has more subcommands to be documented. - if (len(subcommand_table) > 0): - file_name = '%s/index' % command_name - doc.style.tocitem(command_name, file_name=file_name) - else: - doc.style.tocitem(command_name) + doc.style.tocitem(command_name) class OperationDocumentEventHandler(CLIDocumentEventHandler): diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 0b9eaaeb404f..cc559486995f 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -117,8 +117,7 @@ def _build_command_table(self): command_table = self._build_builtin_commands(self.session) self.session.emit('building-command-table.main', command_table=command_table, - session=self.session, - command_object=self) + session=self.session) return command_table def _build_builtin_commands(self, session): @@ -335,10 +334,6 @@ def name(self): def name(self, value): self._name = value - @property - def service_object(self): - return self._service_object - def _get_command_table(self): if self._command_table is None: self._command_table = self._create_command_table() @@ -371,8 +366,7 @@ def _create_command_table(self): service_object=service_object) self.session.emit('building-command-table.%s' % self._name, command_table=command_table, - session=self.session, - command_object=self) + session=self.session) return command_table def create_help_command(self): diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py index 37381960bc7f..0e1a23750daa 100644 --- a/awscli/customizations/commands.py +++ b/awscli/customizations/commands.py @@ -211,8 +211,7 @@ def _build_subcommand_table(self): subcommand_table[subcommand_name] = subcommand_class(self._session) self._session.emit('building-command-table.%s' % self.NAME, command_table=subcommand_table, - session=self._session, - command_object=self) + session=self._session) return subcommand_table def _display_help(self, parsed_args, parsed_globals): diff --git a/awscli/customizations/waiters.py b/awscli/customizations/waiters.py deleted file mode 100644 index c54b45f5fe1c..000000000000 --- a/awscli/customizations/waiters.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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 botocore import xform_name - -from awscli.clidriver import ServiceOperation -from awscli.customizations.commands import BasicCommand, BasicHelp, \ - BasicDocHandler - - -def register_add_waiters(cli): - cli.register('building-command-table', add_waiters) - - -def add_waiters(command_table, session, command_object, **kwargs): - # Check if the command object passed in has a ``service_object``. We - # only want to add wait commands to top level model-driven services. - # These require service objects. - service_object = getattr(command_object, 'service_object', None) - if service_object is not None: - # Get a client out of the service object. - client = translate_service_object_to_client(service_object) - # Find all of the waiters for that client. - waiters = client.waiter_names - # If there are waiters make a wait command. - if waiters: - command_table['wait'] = WaitCommand(client, service_object) - - -def translate_service_object_to_client(service_object): - # Create a client from a service object. - session = service_object.session - return session.create_client(service_object.service_name) - - -class WaitCommand(BasicCommand): - NAME = 'wait' - DESCRIPTION = 'Wait until a particular condition is satisfied.' - - def __init__(self, client, service_object): - self._client = client - self._service_object = service_object - self.waiter_cmd_builder = WaiterStateCommandBuilder( - client=self._client, - service_object=self._service_object - ) - super(WaitCommand, self).__init__(self._service_object.session) - - def _run_main(self, parsed_args, parsed_globals): - if parsed_args.subcommand is None: - raise ValueError("usage: aws [options] " - "[parameters]\naws: error: too few arguments") - - def _build_subcommand_table(self): - subcommand_table = super(WaitCommand, self)._build_subcommand_table() - self.waiter_cmd_builder.build_all_waiter_state_cmds(subcommand_table) - return subcommand_table - - def create_help_command(self): - return BasicHelp(self._session, self, - command_table=self.subcommand_table, - arg_table=self.arg_table, - event_handler_class=WaiterCommandDocHandler) - - -class WaiterStateCommandBuilder(object): - def __init__(self, client, service_object): - self._client = client - self._service_object = service_object - - def build_all_waiter_state_cmds(self, subcommand_table): - """This adds waiter state commands to the subcommand table passed in. - - This is the method that adds waiter state commands like - ``instance-running`` to ``ec2 wait``. - """ - waiters = self._client.waiter_names - for waiter_name in waiters: - waiter_cli_name = waiter_name.replace('_', '-') - subcommand_table[waiter_cli_name] = \ - self._build_waiter_state_cmd(waiter_name) - - def _build_waiter_state_cmd(self, waiter_name): - # Get the waiter - waiter = self._client.get_waiter(waiter_name) - - # Create the cli name for the waiter operation - waiter_cli_name = waiter_name.replace('_', '-') - - # Obtain the name of the service operation that is used to implement - # the specified waiter. - operation_name = waiter.config.operation - - # Create an operation object to make a command for the waiter. The - # operation object is used to generate the arguments for the waiter - # state command. - operation_object = self._service_object.get_operation(operation_name) - waiter_state_command = WaiterStateCommand( - name=waiter_cli_name, parent_name='wait', - operation_object=operation_object, - operation_caller=WaiterCaller(self._client, waiter_name), - service_object=self._service_object - ) - # Build the top level description for the waiter state command. - # Most waiters do not have a description so they need to be generated - # using the waiter configuration. - waiter_state_doc_builder = WaiterStateDocBuilder(waiter.config) - description = waiter_state_doc_builder.build_waiter_state_description() - waiter_state_command.DESCRIPTION = description - return waiter_state_command - - -class WaiterStateDocBuilder(object): - SUCCESS_DESCRIPTIONS = { - 'error': u'%s is thrown ', - 'path': u'%s ', - 'pathAll': u'%s for all elements ', - 'pathAny': u'%s for any element ', - 'status': u'%s response is received ' - } - - def __init__(self, waiter_config): - self._waiter_config = waiter_config - - def build_waiter_state_description(self): - description = self._waiter_config.description - # Use the description provided in the waiter config file. If no - # description is provided, use a heuristic to generate a description - # for the waiter. - if not description: - description = u'Wait until ' - # Look at all of the acceptors and find the success state - # acceptor. - for acceptor in self._waiter_config.acceptors: - # Build the description off of the success acceptor. - if acceptor.state == 'success': - description += self._build_success_description(acceptor) - break - # Include what operation is being used. - description += self._build_operation_description( - self._waiter_config.operation) - return description - - def _build_success_description(self, acceptor): - matcher = acceptor.matcher - # Pick the description template to use based on what the matcher is. - success_description = self.SUCCESS_DESCRIPTIONS[matcher] - resource_description = None - # If success is based off of the state of a resource include the - # description about what resource is looked at. - if matcher in ['path', 'pathAny', 'pathAll']: - resource_description = u'JMESPath query %s returns ' % \ - acceptor.argument - # Prepend the resource description to the template description - success_description = resource_description + success_description - # Complete the description by filling in the expected success state. - full_success_description = success_description % acceptor.expected - return full_success_description - - def _build_operation_description(self, operation): - operation_name = xform_name(operation).replace('_', '-') - return u'when polling with ``%s``.' % operation_name - - -class WaiterCaller(object): - def __init__(self, client, waiter_name): - self._client = client - self._waiter_name = waiter_name - - def invoke(self, operation_object, parameters, parsed_globals): - # Create the endpoint based on the parsed globals - endpoint = operation_object.service.get_endpoint( - region_name=parsed_globals.region, - endpoint_url=parsed_globals.endpoint_url, - verify=parsed_globals.verify_ssl) - # Make a clone of the client using the newly configured endpoint - client = self._client.clone_client(endpoint=endpoint) - # Make the waiter and call its wait method. - client.get_waiter(self._waiter_name).wait(**parameters) - return 0 - - -class WaiterStateCommand(ServiceOperation): - DESCRIPTION = '' - - def create_help_command(self): - help_command = super(WaiterStateCommand, self).create_help_command() - # Change the operation object's description by changing it to the - # description for a waiter state command. - self._operation_object.documentation = self.DESCRIPTION - # Change the output shape because waiters provide no output. - self._operation_object.model.output_shape = None - return help_command - - -class WaiterCommandDocHandler(BasicDocHandler): - def doc_synopsis_start(self, help_command, **kwargs): - pass - - def doc_synopsis_option(self, arg_name, help_command, **kwargs): - pass - - def doc_synopsis_end(self, help_command, **kwargs): - pass - - def doc_options_start(self, help_command, **kwargs): - pass - - def doc_option(self, arg_name, help_command, **kwargs): - pass diff --git a/awscli/handlers.py b/awscli/handlers.py index d7493ee46bfc..e9d2ce941f60 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -51,7 +51,6 @@ from awscli.customizations.cliinputjson import register_cli_input_json from awscli.customizations.generatecliskeleton import \ register_generate_cli_skeleton -from awscli.customizations.waiters import register_add_waiters def awscli_initialize(event_handlers): @@ -106,4 +105,3 @@ def awscli_initialize(event_handlers): register_cloudsearchdomain(event_handlers) register_s3_endpoint(event_handlers) register_generate_cli_skeleton(event_handlers) - register_add_waiters(event_handlers) diff --git a/doc/source/htmlgen b/doc/source/htmlgen index 57593c764ad4..36e5a2461e5f 100755 --- a/doc/source/htmlgen +++ b/doc/source/htmlgen @@ -33,10 +33,8 @@ def do_operation(driver, service_path, operation_name, operation_command): help_command(None, None) -def do_service(driver, ref_path, service_name, service_command, - is_top_level_service=True): - if is_top_level_service: - print('...%s' % service_name) +def do_service(driver, ref_path, service_name, service_command): + print('...%s' % service_name) service_path = os.path.join(ref_path, service_name) if not os.path.isdir(service_path): os.mkdir(service_path) @@ -52,16 +50,7 @@ def do_service(driver, ref_path, service_name, service_command, if operation_name == 'help': continue operation_command = help_command.command_table[operation_name] - subcommand_table = getattr(operation_command, 'subcommand_table', {}) - # If the operation command has a subcommand table with commands - # in it, treat it as a service command as opposed to an operation - # command. - if (len(subcommand_table) > 0): - do_service(driver, service_path, operation_name, - operation_command, False) - else: - do_operation(driver, service_path, operation_name, - operation_command) + do_operation(driver, service_path, operation_name, operation_command) def do_provider(driver): diff --git a/tests/integration/customizations/test_waiters.py b/tests/integration/customizations/test_waiters.py deleted file mode 100644 index b09ca746fe4c..000000000000 --- a/tests/integration/customizations/test_waiters.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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 botocore.session -import random - -from awscli.testutils import unittest, aws - - -class TestDynamoDBWait(unittest.TestCase): - def setUp(self): - self.session = botocore.session.get_session() - self.client = self.session.create_client('dynamodb', 'us-west-2') - - def test_wait_table_exists(self): - # Create a table. - table_name = 'awscliddb-%s' % random.randint(1, 10000) - self.client.create_table( - TableName=table_name, - ProvisionedThroughput={"ReadCapacityUnits": 5, - "WriteCapacityUnits": 5}, - KeySchema=[{"AttributeName": "foo", "KeyType": "HASH"}], - AttributeDefinitions=[{"AttributeName": "foo", - "AttributeType": "S"}]) - self.addCleanup(self.client.delete_table, TableName=table_name) - - # Wait for the table to be active. - p = aws( - 'dynamodb wait table-exists --table-name %s --region us-west-2' % - table_name) - self.assertEqual(p.rc, 0) - - # Make sure the table is active. - parsed = self.client.describe_table(TableName=table_name) - self.assertEqual(parsed['Table']['TableStatus'], 'ACTIVE') diff --git a/tests/unit/customizations/test_waiters.py b/tests/unit/customizations/test_waiters.py deleted file mode 100644 index 73a6dea63497..000000000000 --- a/tests/unit/customizations/test_waiters.py +++ /dev/null @@ -1,334 +0,0 @@ -# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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 mock - -from awscli.testutils import unittest, BaseAWSHelpOutputTest, \ - BaseAWSCommandParamsTest -from awscli.customizations.waiters import add_waiters, WaitCommand, \ - translate_service_object_to_client, WaiterStateCommand, WaiterCaller, \ - WaiterStateDocBuilder, WaiterStateCommandBuilder - - -class TestAddWaiters(unittest.TestCase): - def setUp(self): - self.service_object = mock.Mock() - self.session = mock.Mock() - self.client = mock.Mock() - - self.command_object = mock.Mock() - self.command_object.service_object = self.service_object - - # Set up the mock service object. - self.service_object.session = self.session - - # Set up the mock session. - self.session.create_client.return_value = self.client - - # Set up the mock client. - self.client.waiter_names = ['waiter'] - - def test_add_waiters(self): - command_table = {} - add_waiters(command_table, self.session, self.command_object) - # Make sure a wait command was added. - self.assertIn('wait', command_table) - self.assertIsInstance(command_table['wait'], WaitCommand) - - def test_add_waiters_no_waiter_names(self): - self.client.waiter_names = [] - command_table = {} - add_waiters(command_table, self.session, self.command_object) - # Make sure that no wait command was added since the service object - # has no waiters. - self.assertEqual(command_table, {}) - - def test_add_waiters_no_service_object(self): - command_table = {} - self.command_object.service_object = None - add_waiters(command_table, self.session, self.command_object) - # Make sure that no wait command was added since no service object - # was passed in. - self.assertEqual(command_table, {}) - - -class TestTranslateServiceObjectToClient(unittest.TestCase): - def test_translate_service_object_to_client(self): - service_object = mock.Mock() - session = mock.Mock() - service_object.session = session - service_object.service_name = 'service' - translate_service_object_to_client(service_object) - session.create_client.assert_called_with('service') - - -class TestWaitCommand(unittest.TestCase): - def setUp(self): - self.client = mock.Mock() - self.service_object = mock.Mock() - self.cmd = WaitCommand(self.client, self.service_object) - - def test_run_main_error(self): - self.parsed_args = mock.Mock() - self.parsed_args.subcommand = None - with self.assertRaises(ValueError): - self.cmd._run_main(self.parsed_args, None) - - -class TestWaitHelpOutput(BaseAWSHelpOutputTest): - def test_wait_command_is_in_list(self): - self.driver.main(['ec2', 'help']) - self.assert_contains('* wait') - - def test_wait_help_command(self): - self.driver.main(['ec2', 'wait', 'help']) - self.assert_contains('Wait until a particular condition is satisfied.') - self.assert_contains('* instance-running') - self.assert_contains('* vpc-available') - - def test_wait_state_help_command(self): - self.driver.main(['ec2', 'wait', 'instance-running', 'help']) - self.assert_contains('``describe-instances``') - self.assert_contains('[--filters ]') - self.assert_contains('``--filters`` (list)') - - -class TestWait(BaseAWSCommandParamsTest): - """ This is merely a smoke test. - - Its purpose is to test that the wait command can be run proberly for - various services. It is by no means exhaustive. - """ - def test_ec2_instance_running(self): - cmdline = 'ec2 wait instance-running' - cmdline += ' --instance-ids i-12345678 i-87654321' - cmdline += """ --filters {"Name":"group-name","Values":["foobar"]}""" - result = {'Filter.1.Value.1': 'foobar', 'Filter.1.Name': 'group-name', - 'InstanceId.1': 'i-12345678', 'InstanceId.2': 'i-87654321'} - self.parsed_response = { - 'Reservations': [{ - 'Instances': [{ - 'State': { - 'Name': 'running' - } - }] - }] - } - self.assert_params_for_cmd(cmdline, result) - - def test_dynamodb_table_exists(self): - cmdline = 'dynamodb wait table-exists' - cmdline += ' --table-name mytable' - result = '{"TableName": "mytable"}' - self.parsed_response = {'Table': {'TableStatus': 'ACTIVE'}} - self.assert_params_for_cmd(cmdline, result) - - def test_elastictranscoder_jobs_complete(self): - cmdline = 'rds wait db-instance-available' - cmdline += ' --db-instance-identifier abc' - result = {'DBInstanceIdentifier': 'abc'} - self.parsed_response = { - 'DBInstances': [{ - 'DBInstanceStatus': 'available' - }] - } - self.assert_params_for_cmd(cmdline, result) - - -class TestWaiterStateCommandBuilder(unittest.TestCase): - def setUp(self): - self.client = mock.Mock() - self.service_object = mock.Mock() - - # Create some waiters. - self.client.waiter_names = ['instance_running', 'bucket_exists'] - self.instance_running_waiter = mock.Mock() - self.bucket_exists_waiter = mock.Mock() - - # Make a mock waiter config. - self.waiter_config = mock.Mock() - self.waiter_config.operation = 'MyOperation' - self.waiter_config.description = 'my waiter description' - self.instance_running_waiter.config = self.waiter_config - self.bucket_exists_waiter.config = self.waiter_config - - self.client.get_waiter.side_effect = [ - self.instance_running_waiter, self.bucket_exists_waiter] - - self.waiter_builder = WaiterStateCommandBuilder( - self.client, - self.service_object - ) - - def test_build_waiter_state_cmds(self): - subcommand_table = {} - self.waiter_builder.build_all_waiter_state_cmds(subcommand_table) - # Check the commands are in the command table - self.assertEqual(len(subcommand_table), 2) - self.assertIn('instance-running', subcommand_table) - self.assertIn('bucket-exists', subcommand_table) - - # Make sure that the correct operation object was used. - self.service_object.get_operation.assert_called_with('MyOperation') - - # Introspect the commands in the command table - instance_running_cmd = subcommand_table['instance-running'] - bucket_exists_cmd = subcommand_table['bucket-exists'] - - # Check that the instance type is correct. - self.assertIsInstance(instance_running_cmd, WaiterStateCommand) - self.assertIsInstance(bucket_exists_cmd, WaiterStateCommand) - - # Check the descriptions are set correctly. - self.assertEqual( - instance_running_cmd.DESCRIPTION, - self.waiter_config.description - ) - self.assertEqual( - bucket_exists_cmd.DESCRIPTION, - self.waiter_config.description - ) - - -class TestWaiterStateDocBuilder(unittest.TestCase): - def setUp(self): - self.waiter_config = mock.Mock() - self.waiter_config.description = '' - self.waiter_config.operation = 'MyOperation' - - # Set up the acceptors. - self.success_acceptor = mock.Mock() - self.success_acceptor.state = 'success' - self.fail_acceptor = mock.Mock() - self.fail_acceptor.state = 'failure' - self.error_acceptor = mock.Mock() - self.error_acceptor.state = 'error' - self.waiter_config.acceptors = [ - self.fail_acceptor, - self.success_acceptor, - self.error_acceptor - ] - - self.doc_builder = WaiterStateDocBuilder(self.waiter_config) - - def test_config_provided_description(self): - # Description is provided by the config file - self.waiter_config.description = 'my description' - description = self.doc_builder.build_waiter_state_description() - self.assertEqual(description, 'my description') - - def test_error_acceptor(self): - self.success_acceptor.matcher = 'error' - self.success_acceptor.expected = 'MyException' - description = self.doc_builder.build_waiter_state_description() - self.assertEqual( - description, - 'Wait until MyException is thrown when polling with ' - '``my-operation``.' - ) - - def test_status_acceptor(self): - self.success_acceptor.matcher = 'status' - self.success_acceptor.expected = 200 - description = self.doc_builder.build_waiter_state_description() - self.assertEqual( - description, - 'Wait until 200 response is received when polling with ' - '``my-operation``.' - ) - - def test_path_acceptor(self): - self.success_acceptor.matcher = 'path' - self.success_acceptor.argument = 'MyResource.name' - self.success_acceptor.expected = 'running' - description = self.doc_builder.build_waiter_state_description() - self.assertEqual( - description, - 'Wait until JMESPath query MyResource.name returns running when ' - 'polling with ``my-operation``.' - ) - - def test_path_all_acceptor(self): - self.success_acceptor.matcher = 'pathAll' - self.success_acceptor.argument = 'MyResource[].name' - self.success_acceptor.expected = 'running' - description = self.doc_builder.build_waiter_state_description() - self.assertEqual( - description, - 'Wait until JMESPath query MyResource[].name returns running for ' - 'all elements when polling with ``my-operation``.' - ) - - def test_path_any_acceptor(self): - self.success_acceptor.matcher = 'pathAny' - self.success_acceptor.argument = 'MyResource[].name' - self.success_acceptor.expected = 'running' - description = self.doc_builder.build_waiter_state_description() - self.assertEqual( - description, - 'Wait until JMESPath query MyResource[].name returns running for ' - 'any element when polling with ``my-operation``.' - ) - - -class TestWaiterCaller(unittest.TestCase): - def test_invoke(self): - client = mock.Mock() - waiter = mock.Mock() - waiter_name = 'my_waiter' - operation_object = mock.Mock() - - # Mock the clone of the client - cloned_client = mock.Mock() - cloned_client.get_waiter.return_value = waiter - client.clone_client.return_value = cloned_client - - parameters = {'Foo': 'bar', 'Baz': 'biz'} - parsed_globals = mock.Mock() - parsed_globals.region = 'us-east-1' - parsed_globals.endpoint_url = 'myurl' - parsed_globals.verify_ssl = True - - waiter_caller = WaiterCaller(client, waiter_name) - waiter_caller.invoke(operation_object, parameters, parsed_globals) - # Make sure the endpoint was created properly - operation_object.service.get_endpoint.assert_called_with( - region_name=parsed_globals.region, - endpoint_url=parsed_globals.endpoint_url, - verify=parsed_globals.verify_ssl - ) - # Ensure the client was cloned with using the new endpoint. - clone_kwargs = client.clone_client.call_args[1] - self.assertIn('endpoint', clone_kwargs) - # Ensure we get the waiter. - cloned_client.get_waiter.assert_called_with(waiter_name) - # Ensure the wait command was called properly. - waiter.wait.assert_called_with( - Foo='bar', Baz='biz') - - -class TestWaiterStateCommand(unittest.TestCase): - def test_create_help_command(self): - operation_object = mock.Mock() - operation_object.model.input_shape = None - cmd = WaiterStateCommand( - name='wait-state', parent_name='wait', - operation_object=operation_object, - operation_caller=mock.Mock(), - service_object=mock.Mock() - ) - cmd.DESCRIPTION = 'mydescription' - cmd.create_help_command() - # Make sure that the description is used and output shape is set - # to None for creating the help command. - self.assertEqual(operation_object.documentation, 'mydescription') - self.assertIsNone(operation_object.model.output_shape) diff --git a/tests/unit/test_completer.py b/tests/unit/test_completer.py index 3c22b837e737..6ef516494ef1 100644 --- a/tests/unit/test_completer.py +++ b/tests/unit/test_completer.py @@ -98,7 +98,7 @@ 'modify-cluster-attributes', 'modify-instance-groups', 'put', 'remove-tags', 'restore-from-hbase-backup', 'schedule-hbase-backup', 'socks', 'ssh', - 'terminate-clusters', 'wait'])) + 'terminate-clusters'])) ] From f625b4056a10b74df8bbff05d4696f5723f1e285 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 6 Nov 2014 15:37:38 -0800 Subject: [PATCH 4/6] Update CHANGELOG with new service features --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9bcb9a48cb98..f6360253fa96 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ CHANGELOG ========= +Next Release (TBD) +================== + +* feature:``aws cloudfront``: Adds support for wildcard cookie names and + options caching. +* feature:``aws route53``: Add further support for private dns and sigv4. +* feature:``aws cognito-sync``: Add support for push sync. + + 1.5.5 ===== From 66d026193f813e44e67d03fa9c80af6beccc0ee4 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 6 Nov 2014 15:47:56 -0800 Subject: [PATCH 5/6] Simplify DelegationSetId format --- awscli/customizations/route53resourceid.py | 4 ++-- tests/unit/route53/test_resource_id.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/awscli/customizations/route53resourceid.py b/awscli/customizations/route53resourceid.py index 45b33bfd6bad..8c6130cfabff 100644 --- a/awscli/customizations/route53resourceid.py +++ b/awscli/customizations/route53resourceid.py @@ -22,8 +22,8 @@ def register_resource_id(cli): def _check_for_resource_id(param, value, **kwargs): - if param.name == 'ResourceId': + if param.name in ['ResourceId', 'DelegationSetId']: orig_value = value value = value.split('/')[-1] - logger.debug('ResourceId %s -> %s', orig_value, value) + logger.debug('%s %s -> %s', param.name, orig_value, value) return value diff --git a/tests/unit/route53/test_resource_id.py b/tests/unit/route53/test_resource_id.py index ef529c9d2030..be1e32738846 100644 --- a/tests/unit/route53/test_resource_id.py +++ b/tests/unit/route53/test_resource_id.py @@ -86,6 +86,28 @@ def test_short_resource_id(self): self.assertEqual(self.last_kwargs['Id'], expected_id) +class TestReusableDelegationSet(BaseAWSCommandParamsTest): + + prefix = 'route53 get-reusable-delegation-set' + + def setUp(self): + super(TestReusableDelegationSet, self).setUp() + + def test_full_resource_id(self): + args = ' --id /delegationset/N9INWVYQ6Q0FN' + cmdline = self.prefix + args + expected_id = 'N9INWVYQ6Q0FN' + self.assert_params_for_cmd2(cmdline, {'Id': 'N9INWVYQ6Q0FN'}, + expected_rc=0) + + def test_short_resource_id(self): + args = ' --id N9INWVYQ6Q0FN' + cmdline = self.prefix + args + expected_id = 'N9INWVYQ6Q0FN' + self.assert_params_for_cmd2(cmdline, {'Id': 'N9INWVYQ6Q0FN'}, + expected_rc=0) + + class TestMaxItems(BaseAWSCommandParamsTest): prefix = 'route53 list-resource-record-sets' From ec204f1835f8250e719505d8ec79c980fec5a5f8 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 6 Nov 2014 16:37:19 -0800 Subject: [PATCH 6/6] Bumping version to 1.5.6 --- CHANGELOG.rst | 4 ++-- awscli/__init__.py | 2 +- doc/source/conf.py | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6360253fa96..88645d826717 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,8 @@ CHANGELOG ========= -Next Release (TBD) -================== +1.5.6 +===== * feature:``aws cloudfront``: Adds support for wildcard cookie names and options caching. diff --git a/awscli/__init__.py b/awscli/__init__.py index a6f96146b633..9612d6db8be0 100644 --- a/awscli/__init__.py +++ b/awscli/__init__.py @@ -17,7 +17,7 @@ """ import os -__version__ = '1.5.5' +__version__ = '1.5.6' # # Get our data path to be added to botocore's search path diff --git a/doc/source/conf.py b/doc/source/conf.py index 60aa0d6a21d1..fc6a75f7151f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -52,7 +52,7 @@ # The short X.Y version. version = '1.5' # The full version, including alpha/beta/rc tags. -release = '1.5.5' +release = '1.5.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 1ef8777df2eb..3525a38b9dba 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import awscli -requires = ['botocore>=0.69.0,<0.70.0', +requires = ['botocore>=0.70.0,<0.71.0', 'bcdoc>=0.12.0,<0.13.0', 'six>=1.1.0', 'colorama==0.2.5',