diff --git a/.changes/next-release/feature-Migration-29690.json b/.changes/next-release/feature-Migration-29690.json new file mode 100644 index 000000000000..7e24454e4bdf --- /dev/null +++ b/.changes/next-release/feature-Migration-29690.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "Migration", + "description": "Implement a ``--v2-debug`` flag and ``AWS_CLI_UPGRADE_DEBUG_MODE`` environment variable that detects breaking changes for AWS CLI v2 for entered commands." +} diff --git a/awscli/alias.py b/awscli/alias.py index 29014051ff2e..e09a09c29c68 100644 --- a/awscli/alias.py +++ b/awscli/alias.py @@ -183,7 +183,7 @@ def __call__(self, args, parsed_globals): parsed_alias_args, remaining = self._parser.parse_known_args( alias_args ) - self._update_parsed_globals(parsed_alias_args, parsed_globals) + self._update_parsed_globals(parsed_alias_args, parsed_globals, remaining) # Take any of the remaining arguments that were not parsed out and # prepend them to the remaining args provided to the alias. remaining.extend(args) @@ -228,7 +228,7 @@ def _get_alias_args(self): ) return alias_args - def _update_parsed_globals(self, parsed_alias_args, parsed_globals): + def _update_parsed_globals(self, parsed_alias_args, parsed_globals, remaining): global_params_to_update = self._get_global_parameters_to_update( parsed_alias_args ) @@ -237,7 +237,7 @@ def _update_parsed_globals(self, parsed_alias_args, parsed_globals): # global parameters provided in the alias before updating # the original provided global parameter values # and passing those onto subsequent commands. - emit_top_level_args_parsed_event(self._session, parsed_alias_args) + emit_top_level_args_parsed_event(self._session, parsed_alias_args, remaining) for param_name in global_params_to_update: updated_param_value = getattr(parsed_alias_args, param_name) setattr(parsed_globals, param_name, updated_param_value) diff --git a/awscli/argprocess.py b/awscli/argprocess.py index 14bc648e3edd..af59b08adb37 100644 --- a/awscli/argprocess.py +++ b/awscli/argprocess.py @@ -65,7 +65,7 @@ class TooComplexError(Exception): def unpack_argument( - session, service_name, operation_name, cli_argument, value + session, service_name, operation_name, cli_argument, value, parsed_globals ): """ Unpack an argument's value from the commandline. This is part one of a two @@ -83,6 +83,7 @@ def unpack_argument( value=value, service_name=service_name, operation_name=operation_name, + parsed_globals=parsed_globals, ) if value_override is not None: diff --git a/awscli/clidriver.py b/awscli/clidriver.py index e185fecf0ae4..81429090fdc8 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -48,7 +48,7 @@ ServiceHelpCommand, ) from awscli.plugin import load_plugins -from awscli.utils import emit_top_level_args_parsed_event, write_exception, create_nested_client +from awscli.utils import emit_top_level_args_parsed_event, write_exception, create_nested_client, resolve_v2_debug_mode from botocore import __version__ as botocore_version from botocore import xform_name @@ -225,7 +225,7 @@ def main(self, args=None): # that exceptions can be raised, which should have the same # general exception handling logic as calling into the # command table. This is why it's in the try/except clause. - self._handle_top_level_args(parsed_args) + self._handle_top_level_args(parsed_args, remaining) self._emit_session_event(parsed_args) HISTORY_RECORDER.record( 'CLI_VERSION', self.session.user_agent(), 'CLI' @@ -279,8 +279,8 @@ def _show_error(self, msg): sys.stderr.write(msg) sys.stderr.write('\n') - def _handle_top_level_args(self, args): - emit_top_level_args_parsed_event(self.session, args) + def _handle_top_level_args(self, args, remaining): + emit_top_level_args_parsed_event(self.session, args, remaining) if args.profile: self.session.set_config_variable('profile', args.profile) if args.region: @@ -542,9 +542,15 @@ def __call__(self, args, parsed_globals): event, parsed_args=parsed_args, parsed_globals=parsed_globals ) call_parameters = self._build_call_parameters( - parsed_args, self.arg_table + parsed_args, self.arg_table, parsed_globals ) + self._detect_binary_file_migration_change( + self._session, + parsed_args, + parsed_globals, + self.arg_table + ) event = f'calling-command.{self._parent_name}.{self._name}' override = self._emit_first_non_none_response( event, @@ -590,7 +596,7 @@ def _add_help(self, parser): # CLIArguments for values. parser.add_argument('help', nargs='?') - def _build_call_parameters(self, args, arg_table): + def _build_call_parameters(self, args, arg_table, parsed_globals): # We need to convert the args specified on the command # line as valid **kwargs we can hand to botocore. service_params = {} @@ -601,11 +607,11 @@ def _build_call_parameters(self, args, arg_table): py_name = arg_object.py_name if py_name in parsed_args: value = parsed_args[py_name] - value = self._unpack_arg(arg_object, value) + value = self._unpack_arg(arg_object, value, parsed_globals) arg_object.add_to_params(service_params, value) return service_params - def _unpack_arg(self, cli_argument, value): + def _unpack_arg(self, cli_argument, value, parsed_globals): # Unpacks a commandline argument into a Python value by firing the # load-cli-arg.service-name.operation-name event. session = self._session @@ -613,7 +619,7 @@ def _unpack_arg(self, cli_argument, value): operation_name = xform_name(self._name, '-') return unpack_argument( - session, service_name, operation_name, cli_argument, value + session, service_name, operation_name, cli_argument, value, parsed_globals ) def _create_argument_table(self): @@ -661,6 +667,46 @@ def _create_operation_parser(self, arg_table): parser = ArgTableArgParser(arg_table) return parser + def _detect_binary_file_migration_change( + self, + session, + parsed_args, + parsed_globals, + arg_table + ): + if ( + session.get_scoped_config() + .get('cli_binary_format', None) == 'raw-in-base64-out' + ): + # if cli_binary_format is set to raw-in-base64-out, then v2 behavior will + # be the same as v1, so there is no breaking change in this case. + return + if resolve_v2_debug_mode(parsed_globals): + parsed_args_to_check = { + arg: getattr(parsed_args, arg) + for arg in vars(parsed_args) if getattr(parsed_args, arg) + } + + arg_values_to_check = [ + arg.py_name for arg in arg_table.values() + if arg.py_name in parsed_args_to_check + and arg.argument_model.type_name == 'blob' + ] + if arg_values_to_check: + print( + '\nAWS CLI v2 UPGRADE WARNING: When specifying a ' + 'blob-type parameter, AWS CLI v2 will assume the ' + 'parameter value is base64-encoded. This is different ' + 'from v1 behavior, where the AWS CLI will automatically ' + 'encode the value to base64. To retain v1 behavior in ' + 'AWS CLI v2, set the `cli_binary_format` configuration ' + 'variable to `raw-in-base64-out`. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-binaryparam.\n', + file=sys.stderr + ) + class CLIOperationCaller: """Call an AWS operation and format the response.""" diff --git a/awscli/customizations/cliinputjson.py b/awscli/customizations/cliinputjson.py index 01864e750f1a..47d58c0c3569 100644 --- a/awscli/customizations/cliinputjson.py +++ b/awscli/customizations/cliinputjson.py @@ -70,6 +70,10 @@ def add_to_call_parameters(self, call_parameters, parsed_args, try: # Try to load the JSON string into a python dictionary input_data = json.loads(retrieved_json) + self._session.register( + f"get-cli-input-json-data", + lambda **inner_kwargs: input_data + ) except ValueError as e: raise ParamError( self.name, "Invalid JSON: %s\nJSON received: %s" diff --git a/awscli/customizations/cloudformation/deploy.py b/awscli/customizations/cloudformation/deploy.py index c4f35c2ec45f..08a0a5647ae7 100644 --- a/awscli/customizations/cloudformation/deploy.py +++ b/awscli/customizations/cloudformation/deploy.py @@ -24,7 +24,8 @@ from awscli.customizations.commands import BasicCommand from awscli.compat import get_stdout_text_writer -from awscli.utils import create_nested_client, write_exception +from awscli.customizations.utils import uni_print +from awscli.utils import create_nested_client, write_exception, resolve_v2_debug_mode LOG = logging.getLogger(__name__) @@ -316,18 +317,33 @@ def _run_main(self, parsed_args, parsed_globals): s3_uploader = None deployer = Deployer(cloudformation_client) + v2_debug = resolve_v2_debug_mode(parsed_globals) return self.deploy(deployer, stack_name, template_str, parameters, parsed_args.capabilities, parsed_args.execute_changeset, parsed_args.role_arn, parsed_args.notification_arns, s3_uploader, tags, parsed_args.fail_on_empty_changeset, - parsed_args.disable_rollback) + parsed_args.disable_rollback, v2_debug) def deploy(self, deployer, stack_name, template_str, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags, - fail_on_empty_changeset=True, disable_rollback=False): + fail_on_empty_changeset=True, disable_rollback=False, + v2_debug=False): try: + if v2_debug and fail_on_empty_changeset: + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, deploying ' + 'an AWS CloudFormation Template that results in an empty ' + 'changeset will NOT result in an error by default. This ' + 'is different from v1 behavior, where empty changesets ' + 'result in an error by default. To migrate to v2 behavior ' + 'and resolve this warning, you can add the ' + '`--no-fail-on-empty-changeset` flag to the command. ' + 'See https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-cfn.\n', + out_file=sys.stderr + ) result = deployer.create_and_wait_for_changeset( stack_name=stack_name, cfn_template=template_str, diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py index 45ac54e565ff..8bec7017731a 100644 --- a/awscli/customizations/commands.py +++ b/awscli/customizations/commands.py @@ -154,7 +154,8 @@ def __call__(self, args, parsed_globals): 'custom', self.name, cli_argument, - value + value, + parsed_globals ) # If this parameter has a schema defined, then allow plugins diff --git a/awscli/customizations/globalargs.py b/awscli/customizations/globalargs.py index 93321ccd85aa..b7255708ce4b 100644 --- a/awscli/customizations/globalargs.py +++ b/awscli/customizations/globalargs.py @@ -12,12 +12,18 @@ # language governing permissions and limitations under the License. import sys import os + +from awscli.customizations.argrename import HIDDEN_ALIASES +from awscli.customizations.utils import uni_print from botocore.client import Config from botocore import UNSIGNED from botocore.endpoint import DEFAULT_TIMEOUT +from botocore.useragent import register_feature_id import jmespath from awscli.compat import urlparse +from awscli.utils import resolve_v2_debug_mode + def register_parse_global_args(cli): cli.register('top-level-args-parsed', resolve_types, @@ -30,6 +36,8 @@ def register_parse_global_args(cli): unique_id='resolve-cli-read-timeout') cli.register('top-level-args-parsed', resolve_cli_connect_timeout, unique_id='resolve-cli-connect-timeout') + cli.register('top-level-args-parsed', detect_migration_breakage, + unique_id='detect-migration-breakage') def resolve_types(parsed_args, **kwargs): @@ -90,6 +98,162 @@ def resolve_cli_connect_timeout(parsed_args, session, **kwargs): arg_name = 'connect_timeout' _resolve_timeout(session, parsed_args, arg_name) +def detect_migration_breakage(parsed_args, remaining_args, session, **kwargs): + if not resolve_v2_debug_mode(parsed_args): + return + region = parsed_args.region or session.get_config_variable('region') + s3_config = session.get_config_variable('s3') + if ( + not session.get_scoped_config().get('cli_pager', None) + == '' and 'AWS_PAGER' not in os.environ + ): + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: By default, the AWS CLI v2 returns ' + 'all output through your operating system’s default pager ' + 'program. This is different from v1 behavior, where the system ' + 'pager is not used by default. To retain AWS CLI v1 behavior in ' + 'AWS CLI v2, set the `cli_pager` configuration setting, or the ' + '`AWS_PAGER` environment variable, to the empty string. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-output-pager.\n', + out_file=sys.stderr + ) + if 'PYTHONUTF8' in os.environ or 'PYTHONIOENCODING' in os.environ: + if 'AWS_CLI_FILE_ENCODING' not in os.environ: + uni_print( + '\nThe AWS CLI v2 does not support The `PYTHONUTF8` and ' + '`PYTHONIOENCODING` environment variables, and instead uses ' + 'the `AWS_CLI_FILE_ENCODING` variable. This is different from ' + 'v1 behavior, where the former two variables are used ' + 'instead. To retain AWS CLI v1 behavior in AWS CLI v2, set ' + 'the `AWS_CLI_FILE_ENCODING` environment variable instead. ' + 'See https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-encodingenvvar.\n', + out_file=sys.stderr + ) + if ( + ( + s3_config is None + or s3_config.get('us_east_1_regional_endpoint', 'legacy') + == 'legacy' + ) + and region in ('us-east-1', None) + ): + session.register( + 'request-created.s3.*', + warn_if_east_configured_global_endpoint + ) + session.register( + 'request-created.s3api.*', + warn_if_east_configured_global_endpoint + ) + if session.get_config_variable('api_versions'): + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: AWS CLI v2 UPGRADE WARNING: ' + 'The AWS CLI v2 does not support calling older versions of AWS ' + 'service APIs via the `api_versions` configuration file setting. This ' + 'is different from v1 behavior, where this configuration setting ' + 'can be used to pin older API versions. To migrate to v2 ' + 'behavior, remove the `api_versions` configuration setting, and ' + 'test against the latest service API versions. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-api-versions.\n', + out_file = sys.stderr + ) + if session.full_config.get('plugins', {}): + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, plugins are ' + 'disabled by default, and support for plugins is provisional. ' + 'This is different from v1 behavior, where plugin support is URL ' + 'below to update your configuration to enable plugins in AWS CLI ' + 'v2. Also, be sure to lock into a particular version of the AWS ' + 'CLI and test the functionality of your plugins every time AWS ' + 'CLI v2 is upgraded. See https://docs.aws.amazon.com/cli/latest/' + 'userguide/cliv2-migration-changes.html' + '#cliv2-migration-profile-plugins.\n', + out_file=sys.stderr + ) + if parsed_args.command == 'ecr' and remaining_args[0] == 'get-login': + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: The `ecr get-login` command has ' + 'been removed in AWS CLI v2. You must use `ecr get-login-password` ' + 'instead. See https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-ecr-get-login.\n', + out_file=sys.stderr + ) + for working, obsolete in HIDDEN_ALIASES.items(): + working_split = working.split('.') + working_service = working_split[0] + working_cmd = working_split[1] + working_param = working_split[2] + if ( + parsed_args.command == working_service + and remaining_args[0] == working_cmd + and f"--{working_param}" in remaining_args + ): + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: You have entered command ' + 'arguments that use at least 1 of 21 built-in ("hidden") ' + 'aliases that were removed in AWS CLI v2. For this command ' + 'to work in AWS CLI v2, you must replace usage of the alias ' + 'with the corresponding parameter in AWS CLI v2. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-aliases.\n', + out_file=sys.stderr + ) + # Register against the provide-client-params event to ensure that the + # feature ID is registered before any API requests are made. We + # cannot register the feature ID in this function because no + # botocore context is created at this point. + session.register( + 'provide-client-params.*.*', + _register_v2_debug_feature_id + ) + session.register('choose-signer.s3.*', warn_if_sigv2) + + +def _register_v2_debug_feature_id(params, model, **kwargs): + register_feature_id('CLI_V1_TO_V2_MIGRATION_DEBUG_MODE') + +def warn_if_east_configured_global_endpoint(request, operation_name, **kwargs): + # The regional us-east-1 endpoint is used in certain cases (e.g. + # FIPS/Dual-Stack is enabled). Rather than duplicating this logic + # from botocore, we check the endpoint URL directly. + parsed_url = urlparse.urlparse(request.url) + if parsed_url.hostname.endswith('s3.amazonaws.com'): + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: When you configure AWS CLI v2 to ' + 'use the `us-east-1` region, it uses the true regional endpoint ' + 'rather than the global endpoint. This is different from v1 ' + 'behavior, where the global endpoint would be used when the ' + 'region is `us-east-1`. To retain AWS CLI v1 behavior in AWS ' + 'CLI v2, configure the region setting to `aws-global`. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-s3-regional-endpoint.\n', + out_file=sys.stderr + ) + +def warn_if_sigv2( + signing_name, + region_name, + signature_version, + context, + **kwargs +): + if context.get('auth_type', None) == 'v2': + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: The AWS CLI v2 only uses Signature ' + 'v4 to authenticate Amazon S3 requests. This is different from ' + 'v1 behavior, where the signature used for Amazon S3 requests may ' + 'vary depending on configuration settings, region, and the ' + 'bucket being used. To migrate to AWS CLI v2 behavior, configure ' + 'the Signature Version S3 setting to version 4. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html#cliv2-migration-sigv4.\n', + out_file=sys.stderr + ) def resolve_cli_read_timeout(parsed_args, session, **kwargs): arg_name = 'read_timeout' diff --git a/awscli/customizations/paginate.py b/awscli/customizations/paginate.py index fe1f3f140112..3bae390d7b18 100644 --- a/awscli/customizations/paginate.py +++ b/awscli/customizations/paginate.py @@ -33,7 +33,7 @@ from botocore import model from awscli.arguments import BaseCLIArgument - +from awscli.utils import resolve_v2_debug_mode logger = logging.getLogger(__name__) @@ -135,6 +135,9 @@ def unify_paging_params(argument_table, operation_model, event_name, _remove_existing_paging_arguments(argument_table, paginator_config) parsed_args_event = event_name.replace('building-argument-table.', 'operation-args-parsed.') + call_parameters_event = event_name.replace( + 'building-argument-table', 'calling-command' + ) shadowed_args = {} add_paging_argument(argument_table, 'starting-token', PageArgument('starting-token', STARTING_TOKEN_HELP, @@ -168,6 +171,14 @@ def unify_paging_params(argument_table, operation_model, event_name, partial(check_should_enable_pagination, list(_get_all_cli_input_tokens(paginator_config)), shadowed_args, argument_table)) + session.register( + call_parameters_event, + partial( + check_should_enable_pagination_call_parameters, + session, + list(_get_all_input_tokens(paginator_config)), + ), + ) def add_paging_argument(argument_table, arg_name, argument, shadowed_args): @@ -240,6 +251,18 @@ def _get_all_cli_input_tokens(pagination_config): yield cli_name +# Get all tokens but return them in API namespace rather than CLI namespace +def _get_all_input_tokens(pagination_config): + # Get all input tokens including the limit_key + # if it exists. + tokens = _get_input_tokens(pagination_config) + for token_name in tokens: + yield token_name + if 'limit_key' in pagination_config: + key_name = pagination_config['limit_key'] + yield key_name + + def _get_input_tokens(pagination_config): tokens = pagination_config['input_token'] if not isinstance(tokens, list): @@ -253,6 +276,48 @@ def _get_cli_name(param_objects, token_name): return param.cli_name.lstrip('-') +def check_should_enable_pagination_call_parameters( + session, + input_tokens, + call_parameters, + parsed_args, + parsed_globals, + **kwargs +): + """ + Check for pagination args in the actual calling arguments passed to + the function. + + If the user is using the --cli-input-json parameter to provide JSON + parameters they are all in the API naming space rather than the CLI + naming space and would be missed by the processing above. This function + gets called on the calling-command event. + """ + if resolve_v2_debug_mode(parsed_globals): + cli_input_json_data = session.emit_first_non_none_response( + f"get-cli-input-json-data", + ) + if cli_input_json_data is None: + cli_input_json_data = {} + pagination_params_in_input_tokens = [ + param for param in cli_input_json_data if param in input_tokens + ] + if pagination_params_in_input_tokens: + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify ' + 'pagination parameters by using a file with the ' + '`--cli-input-json` parameter, automatic pagination will be ' + 'turned off. This is different from v1 behavior, where ' + 'pagination parameters specified via the `--cli-input-json` ' + 'parameter are ignored. To retain AWS CLI v1 behavior in ' + 'AWS CLI v2, remove all pagination parameters from the input ' + 'JSON. See https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-skeleton-paging.\n', + out_file=sys.stderr + ) + + class PageArgument(BaseCLIArgument): type_map = { 'string': str, diff --git a/awscli/customizations/s3/subcommands.py b/awscli/customizations/s3/subcommands.py index 3f3a2834c6d5..47ef10e88eec 100644 --- a/awscli/customizations/s3/subcommands.py +++ b/awscli/customizations/s3/subcommands.py @@ -36,7 +36,7 @@ from awscli.customizations.s3.syncstrategy.base import MissingFileSync, \ SizeAndLastModifiedSync, NeverSync from awscli.customizations.s3 import transferconfig - +from awscli.utils import resolve_v2_debug_mode LOGGER = logging.getLogger(__name__) @@ -767,6 +767,7 @@ def _run_main(self, parsed_args, parsed_globals): cmd_params.add_verify_ssl(parsed_globals) cmd_params.add_page_size(parsed_args) cmd_params.add_paths(parsed_args.paths) + cmd_params.add_v2_debug(parsed_globals) runtime_config = transferconfig.RuntimeConfig().build_config( **self._session.get_scoped_config().get('s3', {})) @@ -1056,6 +1057,24 @@ def run(self): result_queue = queue.Queue() operation_name = cmd_translation[paths_type] + if self.parameters['v2_debug']: + if operation_name == 'copy': + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, object ' + 'properties will be copied from the source in multipart ' + 'copies between S3 buckets initiated via `aws s3` ' + 'commands, resulting in additional S3 API calls to ' + 'transfer the metadata. Note that the principal must ' + 'have permission to call these APIs, or the command may ' + 'fail. This is different from v1 behavior, where metadata ' + 'is not copied. For guidance on retaining v1 behavior in ' + 'AWS CLI v2, or for more details, see ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-s3-copy-metadata.\n\n', + out_file=sys.stderr + ) + fgen_kwargs = { 'client': self._source_client, 'operation_name': operation_name, 'follow_symlinks': self.parameters['follow_symlinks'], @@ -1447,6 +1466,9 @@ def add_verify_ssl(self, parsed_globals): def add_page_size(self, parsed_args): self.parameters['page_size'] = getattr(parsed_args, 'page_size', None) + def add_v2_debug(self, parsed_globals): + self.parameters['v2_debug'] = resolve_v2_debug_mode(parsed_globals) + def _validate_sse_c_args(self): self._validate_sse_c_arg() self._validate_sse_c_arg('sse_c_copy_source') diff --git a/awscli/customizations/scalarparse.py b/awscli/customizations/scalarparse.py index d2051007a14b..8ddf868b4984 100644 --- a/awscli/customizations/scalarparse.py +++ b/awscli/customizations/scalarparse.py @@ -27,9 +27,14 @@ in the future. """ +import sys + from botocore.utils import parse_timestamp from botocore.exceptions import ProfileNotFound +from awscli.customizations.utils import uni_print +from awscli.utils import resolve_v2_debug_mode + def register_scalar_parser(event_handlers): event_handlers.register_first( @@ -44,12 +49,20 @@ def iso_format(value): return parse_timestamp(value).isoformat() -def add_timestamp_parser(session): +def add_timestamp_parser(session, v2_debug): factory = session.get_component('response_parser_factory') + print_v2_debug_warnings = v2_debug try: timestamp_format = session.get_scoped_config().get( 'cli_timestamp_format', - 'wire') + None) + if timestamp_format is not None: + # We do not want to print v2 debug warnings if the user explicitly + # configured the cli_timestamp_format, they would not be + # broken in that case. + print_v2_debug_warnings = False + else: + timestamp_format = 'wire' except ProfileNotFound: # If a --profile is provided that does not exist, loading # a value from get_scoped_config will crash the CLI. @@ -66,7 +79,31 @@ def add_timestamp_parser(session): # parser (which parses to a datetime.datetime object) with the # identity function which prints the date exactly the same as it comes # across the wire. - timestamp_parser = identity + encountered_timestamp = False + def identity_with_warning(x): + # To prevent printing the same warning for each timestamp in the + # response, we utilize a reference to a nonlocal variable to track + # if we have already printed the warning. + nonlocal encountered_timestamp + if not encountered_timestamp: + encountered_timestamp = True + uni_print( + '\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, all ' + 'timestamp response values are returned in the ISO 8601 ' + 'format. This is different from v1 behavior, where the ' + 'timestamps are returned as they appear in the service ' + 'API response. To retain AWS CLI v1 behavior in AWS CLI ' + 'v2, set the configuration variable ' + '`cli_timestamp_format` to `wire`. See ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-timestamp.\n', + out_file=sys.stderr + ) + return identity(x) + + timestamp_parser = identity_with_warning \ + if print_v2_debug_warnings else identity elif timestamp_format == 'iso8601': timestamp_parser = iso_format else: @@ -75,7 +112,7 @@ def add_timestamp_parser(session): factory.set_parser_defaults(timestamp_parser=timestamp_parser) -def add_scalar_parsers(session, **kwargs): +def add_scalar_parsers(session, parsed_args, **kwargs): factory = session.get_component('response_parser_factory') factory.set_parser_defaults(blob_parser=identity) - add_timestamp_parser(session) + add_timestamp_parser(session, resolve_v2_debug_mode(parsed_args)) diff --git a/awscli/data/cli.json b/awscli/data/cli.json index 85a2efebf537..25687399d05c 100644 --- a/awscli/data/cli.json +++ b/awscli/data/cli.json @@ -64,6 +64,11 @@ "dest": "connect_timeout", "type": "int", "help": "

The maximum socket connect time in seconds. If the value is set to 0, the socket connect will be blocking and not timeout. The default value is 60 seconds.

" + }, + "v2-debug": { + "action": "store_true", + "dest": "v2_debug", + "help": "

Enable AWS CLI v2 migration assistance. Prints warnings if the command would face a breaking change after swapping AWS CLI v1 for AWS CLI v2 in the current environment. Prints one warning for each breaking change detected.

" } } } diff --git a/awscli/examples/global_options.rst b/awscli/examples/global_options.rst index 2f8c7115e8ae..d450cf0ce78a 100644 --- a/awscli/examples/global_options.rst +++ b/awscli/examples/global_options.rst @@ -70,3 +70,7 @@ The maximum socket connect time in seconds. If the value is set to 0, the socket connect will be blocking and not timeout. The default value is 60 seconds. +``--v2-debug`` (boolean) + + Enable AWS CLI v2 migration assistance. Prints warnings if the command would face a breaking change after swapping AWS CLI v1 for AWS CLI v2 in the current environment. Prints one warning for each breaking change detected. + diff --git a/awscli/examples/global_synopsis.rst b/awscli/examples/global_synopsis.rst index c5baaa9583ba..12865958a809 100644 --- a/awscli/examples/global_synopsis.rst +++ b/awscli/examples/global_synopsis.rst @@ -12,3 +12,4 @@ [--ca-bundle ] [--cli-read-timeout ] [--cli-connect-timeout ] +[--v2-debug] diff --git a/awscli/paramfile.py b/awscli/paramfile.py index 14cd7fc49330..baaa28f63150 100644 --- a/awscli/paramfile.py +++ b/awscli/paramfile.py @@ -10,6 +10,8 @@ # 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 sys + import copy import logging import os @@ -20,6 +22,7 @@ from awscli import argprocess from awscli.compat import compat_open +from awscli.utils import resolve_v2_debug_mode logger = logging.getLogger(__name__) @@ -166,7 +169,7 @@ def __init__(self, prefixes=None): prefixes.update(REMOTE_PREFIX_MAP) self._prefixes = prefixes - def __call__(self, event_name, param, value, **kwargs): + def __call__(self, event_name, param, value, parsed_globals, **kwargs): """Handler that supports param values from URIs.""" cli_argument = param qualified_param_name = '.'.join(event_name.split('.')[1:]) @@ -175,13 +178,27 @@ def __call__(self, event_name, param, value, **kwargs): ): return else: - return self._check_for_uri_param(cli_argument, value) + return self._check_for_uri_param(cli_argument, value, parsed_globals) - def _check_for_uri_param(self, param, value): + def _check_for_uri_param(self, param, value, parsed_globals): if isinstance(value, list) and len(value) == 1: value = value[0] try: - return get_paramfile(value, self._prefixes) + param_file = get_paramfile(value, self._prefixes) + if param_file is not None and resolve_v2_debug_mode(parsed_globals): + print( + '\nAWS CLI v2 UPGRADE WARNING: For input parameters that ' + 'have a prefix of `http://` or `https://`, AWS CLI v2 ' + 'will not automatically request the content of the URL ' + 'for the parameter, and the `cli_follow_urlparam` option ' + 'has been removed. For guidance on how to adapt this ' + 'command to AWS CLI v2 usage, see ' + 'https://docs.aws.amazon.com/cli/latest/userguide/' + 'cliv2-migration-changes.html' + '#cliv2-migration-paramfile.\n', + file=sys.stderr, + ) + return param_file except ResourceLoadingError as e: raise argprocess.ParamError(param.cli_name, str(e)) diff --git a/awscli/utils.py b/awscli/utils.py index 4a3096320fad..b8e5782418b4 100644 --- a/awscli/utils.py +++ b/awscli/utils.py @@ -134,6 +134,16 @@ def find_service_and_method_in_event_name(event_name): return service_name, operation_name +def resolve_v2_debug_mode(args): + # Resolve whether v2-debug mode is enabled, + # following the correct precedence order. + if getattr(args, 'v2_debug', False): + return True + if os.environ.get('AWS_CLI_UPGRADE_DEBUG_MODE', '').lower() == 'true': + return True + return False + + def is_document_type(shape): """Check if shape is a document type""" return getattr(shape, 'is_document_type', False) @@ -205,8 +215,13 @@ def ignore_ctrl_c(): signal.signal(signal.SIGINT, original) -def emit_top_level_args_parsed_event(session, args): - session.emit('top-level-args-parsed', parsed_args=args, session=session) +def emit_top_level_args_parsed_event(session, args, remaining): + session.emit( + 'top-level-args-parsed', + parsed_args=args, + remaining_args=remaining, + session=session + ) def is_a_tty(): diff --git a/tests/functional/test_api_versions.py b/tests/functional/test_api_versions.py index c30da1b6a56a..84748819b421 100644 --- a/tests/functional/test_api_versions.py +++ b/tests/functional/test_api_versions.py @@ -10,7 +10,7 @@ # 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 awscli.testutils import create_clidriver +from awscli.testutils import create_clidriver, capture_output from awscli.testutils import BaseAWSCommandParamsTest, FileCreator @@ -49,3 +49,19 @@ def test_command_interface_reflects_api_version(self): cmdline = 'ec2 describe-nat-gateways' _, stderr, _ = self.run_cmd(cmdline, expected_rc=2) self.assertIn("Invalid choice: 'describe-nat-gateways'", stderr) + + def test_v2_debug_migration_warning(self): + cmdline = 'ec2 describe-instances --v2-debug' + _, stderr, _ = self.run_cmd(cmdline) + # Make sure that the correct api version is used for the client + # by checking the version that was sent in the request. + self.assertEqual(self.last_params['Version'], self.api_version) + # Make sure that the migration warning is printed since the user + # specified --v2-debug + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: AWS CLI v2 UPGRADE WARNING: The AWS ' + 'CLI v2 does not support calling older versions of AWS service ' + 'APIs via the `api_versions` configuration file setting.', + stderr + ) + diff --git a/tests/unit/customizations/cloudformation/test_deploy.py b/tests/unit/customizations/cloudformation/test_deploy.py index 8aac8ae3be4f..894bb3dc9aa5 100644 --- a/tests/unit/customizations/cloudformation/test_deploy.py +++ b/tests/unit/customizations/cloudformation/test_deploy.py @@ -120,7 +120,8 @@ def test_command_invoked(self, mock_yaml_parse): None, fake_tags, True, - True + True, + False ) self.deploy_command.parse_key_value_arg.assert_has_calls([ @@ -207,7 +208,8 @@ def test_s3_uploader_is_configured_properly(self, s3UploaderMock, s3UploaderObject, [{"Key": "tagkey1", "Value": "tagvalue1"}], True, - True + True, + False ) s3UploaderMock.assert_called_once_with(mock.ANY, diff --git a/tests/unit/customizations/s3/test_subcommands.py b/tests/unit/customizations/s3/test_subcommands.py index 09dd0a3db667..25fc257fcb21 100644 --- a/tests/unit/customizations/s3/test_subcommands.py +++ b/tests/unit/customizations/s3/test_subcommands.py @@ -450,7 +450,8 @@ def test_run_cp_put(self): 'paths_type': 'locals3', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': None, 'metadata': None} + 'is_stream': False, 'source_region': None, 'metadata': None, + 'v2_debug': False} config = RuntimeConfig().build_config() cmd_arc = CommandArchitecture(self.session, 'cp', params, config) cmd_arc.set_clients() @@ -470,7 +471,8 @@ def test_error_on_same_line_as_status(self): 'paths_type': 'locals3', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': None, 'metadata': None} + 'is_stream': False, 'source_region': None, 'metadata': None, + 'v2_debug': False} self.http_response.status_code = 400 self.parsed_responses = [{'Error': { 'Code': 'BucketNotExists', @@ -501,7 +503,7 @@ def test_run_cp_get(self): 'paths_type': 's3local', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': None} + 'is_stream': False, 'source_region': None, 'v2_debug': False} self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100, "LastModified": "2014-01-09T20:45:49.000Z"}] config = RuntimeConfig().build_config() @@ -524,7 +526,7 @@ def test_run_cp_copy(self): 'paths_type': 's3s3', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': None} + 'is_stream': False, 'source_region': None, 'v2_debug': False} self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100, "LastModified": "2014-01-09T20:45:49.000Z"}] config = RuntimeConfig().build_config() @@ -548,7 +550,7 @@ def test_run_mv(self): 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, 'is_stream': False, 'source_region': None, - 'is_move': True} + 'is_move': True, 'v2_debug': False} self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100, "LastModified": "2014-01-09T20:45:49.000Z"}] config = RuntimeConfig().build_config() @@ -571,7 +573,8 @@ def test_run_remove(self): 'paths_type': 's3', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': None} + 'is_stream': False, 'source_region': None, + 'v2_debug': False} self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100, "LastModified": "2014-01-09T20:45:49.000Z"}] config = RuntimeConfig().build_config() @@ -598,7 +601,8 @@ def test_run_sync(self): 'paths_type': 'locals3', 'region': 'us-east-1', 'endpoint_url': None, 'verify_ssl': None, 'follow_symlinks': True, 'page_size': None, - 'is_stream': False, 'source_region': 'us-west-2'} + 'is_stream': False, 'source_region': 'us-west-2', + 'v2_debug': False} self.parsed_responses = [ {"CommonPrefixes": [], "Contents": [ {"Key": "text1.txt", "Size": 100, @@ -613,6 +617,34 @@ def test_run_sync(self): output_str = "(dryrun) upload: %s to %s" % (rel_local_file, s3_file) self.assertIn(output_str, self.output.getvalue()) + def test_v2_debug_mv(self): + s3_file = 's3://' + self.bucket + '/' + 'text1.txt' + filters = [['--include', '*']] + params = {'dir_op': False, 'quiet': False, 'dryrun': True, + 'src': s3_file, 'dest': s3_file, 'filters': filters, + 'paths_type': 's3s3', 'region': 'us-east-1', + 'endpoint_url': None, 'verify_ssl': None, + 'follow_symlinks': True, 'page_size': None, + 'is_stream': False, 'source_region': None, + 'is_move': True, 'v2_debug': True} + self.parsed_responses = [{"ETag": "abcd", "ContentLength": 100, + "LastModified": "2014-01-09T20:45:49.000Z"}] + config = RuntimeConfig().build_config() + cmd_arc = CommandArchitecture(self.session, 'mv', params, config) + cmd_arc.set_clients() + cmd_arc.create_instructions() + self.patch_make_request() + cmd_arc.run() + warning_str = ( + 'AWS CLI v2 UPGRADE WARNING: In AWS CLI v2, object ' + 'properties will be copied from the source in ' + 'multipart copies between S3 buckets initiated via ' + '`aws s3` commands' + ) + output_str = f"(dryrun) move: {s3_file} to {s3_file}" + self.assertIn(warning_str, self.err_output.getvalue()) + self.assertIn(output_str, self.output.getvalue()) + class CommandParametersTest(unittest.TestCase): def setUp(self): diff --git a/tests/unit/customizations/test_globalargs.py b/tests/unit/customizations/test_globalargs.py index 8316b7c9229b..d53438f5c095 100644 --- a/tests/unit/customizations/test_globalargs.py +++ b/tests/unit/customizations/test_globalargs.py @@ -10,11 +10,13 @@ # 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 unittest.mock import patch, Mock + from botocore.session import get_session from botocore import UNSIGNED import os -from awscli.testutils import mock, unittest +from awscli.testutils import mock, unittest, capture_output from awscli.customizations import globalargs @@ -185,3 +187,175 @@ def test_cli_connect_timeout_for_blocking(self): self.assertEqual(parsed_args.connect_timeout, None) self.assertEqual( session.get_default_client_config().connect_timeout, None) + + def test_register_feature_id(self): + parsed_args = FakeParsedArgs(v2_debug=True) + session = get_session() + globalargs.detect_migration_breakage( + parsed_args, + [], + session + ) + # Verify the correct feature ID is registered during the + # provide-client-params event. + with (mock.patch('awscli.customizations.globalargs.register_feature_id') + as mock_register_feature_id): + session.emit( + 'provide-client-params.s3.ListBuckets', + params={}, + model={}, + ) + mock_register_feature_id.assert_any_call( + 'CLI_V1_TO_V2_MIGRATION_DEBUG_MODE' + ) + + def test_ecr_login_v2_debug(self): + parsed_args = FakeParsedArgs(command='ecr', v2_debug=True) + remaining_args = ['get-login'] + session = get_session() + with capture_output() as output: + globalargs.detect_migration_breakage( + parsed_args, + remaining_args, + session + ) + # Verify the expected warning is printed + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: The `ecr get-login` command has ' + 'been removed in AWS CLI v2.', + output.stderr.getvalue() + ) + + def test_ecr_login_v2_debug_env_var(self): + parsed_args = FakeParsedArgs(command='ecr') + remaining_args = ['get-login'] + session = get_session() + env = {'AWS_CLI_UPGRADE_DEBUG_MODE': 'true'} + with capture_output() as output: + with mock.patch('os.environ', env): + globalargs.detect_migration_breakage( + parsed_args, + remaining_args, + session + ) + # Verify the expected warning is printed + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: The `ecr get-login` command has ' + 'been removed in AWS CLI v2.', + output.stderr.getvalue() + ) + + def test_v2_debug_python_utf8_env_var(self): + parsed_args = FakeParsedArgs(v2_debug=True) + session = get_session() + environ = {'PYTHONUTF8': '1'} + with mock.patch('os.environ', environ): + with capture_output() as output: + globalargs.detect_migration_breakage(parsed_args, [], session) + self.assertIn( + 'The AWS CLI v2 does not support The `PYTHONUTF8` and ' + '`PYTHONIOENCODING` environment variables, and instead ' + 'uses the `AWS_CLI_FILE_ENCODING` variable', + output.stderr.getvalue() + ) + + def test_v2_debug_python_utf8_resolved_env_var(self): + parsed_args = FakeParsedArgs(v2_debug=True) + session = get_session() + environ = {'PYTHONUTF8': '1', 'AWS_CLI_FILE_ENCODING': 'UTF-8'} + with mock.patch('os.environ', environ): + with capture_output() as output: + globalargs.detect_migration_breakage(parsed_args, [], session) + self.assertNotIn( + 'AWS CLI v2 UPGRADE WARNING: The PYTHONUTF8 and ' + 'PYTHONIOENCODING environment variables are unsupported ' + 'in AWS CLI v2.', + output.stderr.getvalue() + ) + + def test_v2_debug_python_io_encoding_env_var(self): + parsed_args = FakeParsedArgs(v2_debug=True) + session = get_session() + environ = {'PYTHONIOENCODING': 'UTF8'} + with mock.patch('os.environ', environ): + with capture_output() as output: + globalargs.detect_migration_breakage(parsed_args, [], session) + self.assertIn( + 'The AWS CLI v2 does not support The `PYTHONUTF8` and ' + '`PYTHONIOENCODING` environment variables, and instead ' + 'uses the `AWS_CLI_FILE_ENCODING` variable', + output.stderr.getvalue() + ) + + def test_v2_debug_s3_sigv2(self): + parsed_args = FakeParsedArgs(v2_debug=True) + session = get_session() + globalargs.detect_migration_breakage(parsed_args, [], session) + with capture_output() as output: + session.emit( + 'choose-signer.s3.*', + signing_name='s3', + region_name='us-west-2', + signature_version='v2', + context={'auth_type': 'v2'}, + ) + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: The AWS CLI v2 only uses Signature ' + 'v4 to authenticate Amazon S3 requests.', + output.stderr.getvalue() + ) + + def test_v2_debug_s3_us_east_1(self): + parsed_args = FakeParsedArgs(v2_debug=True, region='us-east-1') + session = get_session() + globalargs.detect_migration_breakage(parsed_args, [], session) + def mock_get(key: str): + if key == 'retries': + return {'invocation-id': '012345'} + return None + + with capture_output() as output: + mock_request = Mock() + mock_request.url = 'https://s3.amazonaws.com' + mock_request.context.get.side_effect = mock_get + mock_request.headers = {} + + session.emit( + 'request-created.s3.ListBuckets', + request=mock_request, + operation_name='ListBuckets', + ) + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: When you configure AWS CLI v2 ' + 'to use the `us-east-1` region, it uses the true regional ' + 'endpoint rather than the global endpoint.', + output.stderr.getvalue() + ) + + def test_v2_debug_s3api_us_east_1(self): + parsed_args = FakeParsedArgs(v2_debug=True, region='us-east-1') + session = get_session() + globalargs.detect_migration_breakage(parsed_args, [], session) + + def mock_get(key: str): + if key == 'retries': + return {'invocation-id': '012345'} + return None + + with capture_output() as output: + mock_request = Mock() + mock_request.url = 'https://s3.amazonaws.com' + mock_request.context.get.side_effect = mock_get + mock_request.headers = {} + + session.emit( + 'request-created.s3api.ListBuckets', + request=mock_request, + operation_name='ListBuckets', + ) + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: When you configure AWS CLI v2 ' + 'to use the `us-east-1` region, it uses the true regional ' + 'endpoint rather than the global endpoint.', + output.stderr.getvalue() + ) \ No newline at end of file diff --git a/tests/unit/customizations/test_paginate.py b/tests/unit/customizations/test_paginate.py index cb362a0fc631..220f4feb8499 100644 --- a/tests/unit/customizations/test_paginate.py +++ b/tests/unit/customizations/test_paginate.py @@ -10,10 +10,12 @@ # 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 functools import partial + import pytest from awscli.customizations.paginate import PageArgument -from awscli.testutils import mock, unittest +from awscli.testutils import mock, unittest, capture_output from botocore.exceptions import DataNotFoundError, PaginationError from botocore.model import OperationModel @@ -215,6 +217,7 @@ def setUp(self): self.parsed_args.starting_token = None self.parsed_args.page_size = None self.parsed_args.max_items = None + self.call_parameters = {} def test_should_not_enable_pagination(self): # Here the user has specified a manual pagination argument, @@ -307,6 +310,91 @@ def test_shadowed_args_are_replaced_when_pagination_set_off(self): self.assertEqual(arg_table['foo'], mock.sentinel.ORIGINAL_ARG) +class TestPaginateV2Debug(TestPaginateBase): + def setUp(self): + super().setUp() + self.parsed_globals = mock.Mock() + self.parsed_args = mock.Mock() + self.parsed_args.starting_token = None + self.parsed_args.page_size = None + self.parsed_args.max_items = None + self.call_parameters = {} + + def _mock_emit_first_non_none_response( + self, + mock_input_json_data, + event_name + ): + if event_name == 'get-cli-input-json-data': + return mock_input_json_data + return None + + def test_v2_debug_call_parameters(self): + # Here the user has specified a manual pagination argument, + # via CLI Input JSON and specified v2-debug, so a + # migration warning should be printed. + # From setUp(), the limit_key is 'Bar' + input_tokens = ['Foo', 'Bar'] + self.parsed_globals.v2_debug = True + self.parsed_globals.paginate = True + self.session.emit_first_non_none_response.side_effect = partial( + self._mock_emit_first_non_none_response, + {'Bar': 10} + ) + # Corresponds to --bar 10 + self.call_parameters['Foo'] = None + self.call_parameters['Bar'] = 10 + with capture_output() as output: + paginate.check_should_enable_pagination_call_parameters( + self.session, + input_tokens, + self.call_parameters, + {}, + self.parsed_globals + ) + # We should have printed the migration warning + # because the user specified {Bar: 10} in the input JSON + self.assertIn( + 'AWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify ' + 'pagination parameters by using a file with the ' + '`--cli-input-json` parameter, automatic pagination will be ' + 'turned off.', + output.stderr.getvalue() + ) + + def test_v2_debug_call_params_does_not_print_for_cmd_args(self): + # Here the user has specified a pagination argument as a command + # argument and specified v2-debug, so the migration warning should NOT + # be printed. From setUp(), the limit_key is 'Bar' + input_tokens = ['Foo', 'Bar'] + self.parsed_globals.v2_debug = True + self.parsed_globals.paginate = True + self.session.emit_first_non_none_response.side_effect = partial( + self._mock_emit_first_non_none_response, + None + ) + # Corresponds to --bar 10 + self.call_parameters['Foo'] = None + self.call_parameters['Bar'] = 10 + with capture_output() as output: + paginate.check_should_enable_pagination_call_parameters( + self.session, + input_tokens, + self.call_parameters, + {}, + self.parsed_globals + ) + # We should not have printed the warning because + # the user did not specify any params through CLI input JSON + self.assertNotIn( + 'AWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify ' + 'pagination parameters by using a file with the ' + '`--cli-input-json` parameter, automatic pagination will be ' + 'turned off.', + output.stderr.getvalue() + ) + + class TestEnsurePagingParamsNotSet(TestPaginateBase): def setUp(self): super(TestEnsurePagingParamsNotSet, self).setUp() diff --git a/tests/unit/customizations/test_scalarparse.py b/tests/unit/customizations/test_scalarparse.py index 7b1b19919247..b5cbfd10be23 100644 --- a/tests/unit/customizations/test_scalarparse.py +++ b/tests/unit/customizations/test_scalarparse.py @@ -32,7 +32,7 @@ def test_scalar_parsers_set(self): session = mock.Mock() session.get_scoped_config.return_value = {'cli_timestamp_format': 'none'} - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) session.get_component.assert_called_with('response_parser_factory') factory = session.get_component.return_value expected = [mock.call(blob_parser=scalarparse.identity), @@ -45,7 +45,7 @@ def test_choose_none_timestamp_formatter(self): session.get_scoped_config.return_value = {'cli_timestamp_format': 'none'} factory = session.get_component.return_value - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) factory.set_parser_defaults.assert_called_with( timestamp_parser=scalarparse.identity) @@ -54,7 +54,7 @@ def test_choose_wire_timestamp_formatter(self): session.get_scoped_config.return_value = {'cli_timestamp_format': 'wire'} factory = session.get_component.return_value - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) factory.set_parser_defaults.assert_called_with( timestamp_parser=scalarparse.identity) @@ -63,7 +63,7 @@ def test_choose_iso_timestamp_formatter(self): session.get_scoped_config.return_value = {'cli_timestamp_format': 'iso8601'} factory = session.get_component.return_value - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) factory.set_parser_defaults.assert_called_with( timestamp_parser=scalarparse.iso_format) @@ -73,12 +73,12 @@ def test_choose_invalid_timestamp_formatter(self): 'foobar'} session.get_component.return_value with self.assertRaises(ValueError): - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) def test_choose_timestamp_parser_profile_not_found(self): session = mock.Mock(spec=Session) session.get_scoped_config.side_effect = ProfileNotFound(profile='foo') factory = session.get_component.return_value - scalarparse.add_scalar_parsers(session) + scalarparse.add_scalar_parsers(session, mock.Mock(v2_debug=False)) factory.set_parser_defaults.assert_called_with( timestamp_parser=scalarparse.identity) diff --git a/tests/unit/test_alias.py b/tests/unit/test_alias.py index 853aa6cd903f..7707cf319c93 100644 --- a/tests/unit/test_alias.py +++ b/tests/unit/test_alias.py @@ -396,7 +396,7 @@ def replace_global_param_value_with_foo(event_name, **kwargs): alias_cmd([], FakeParsedArgs(command=self.alias_name)) self.session.emit.assert_called_with( 'top-level-args-parsed', parsed_args=mock.ANY, - session=self.session) + session=self.session, remaining_args=mock.ANY) command_table['myservice'].assert_called_with( ['myoperation'], diff --git a/tests/unit/test_argprocess.py b/tests/unit/test_argprocess.py index e59eb6c3b64c..e37398d82145 100644 --- a/tests/unit/test_argprocess.py +++ b/tests/unit/test_argprocess.py @@ -80,7 +80,7 @@ def test_uri_param(self): ) f.write(json_argument) f.flush() - result = self.uri_param('event-name', p, 'file://%s' % f.name) + result = self.uri_param('event-name', p, 'file://%s' % f.name, mock.Mock()) self.assertEqual(result, json_argument) def test_uri_param_no_paramfile_false(self): @@ -90,7 +90,7 @@ def test_uri_param_no_paramfile_false(self): json_argument = json.dumps([{"Name": "instance-id", "Values": ["i-1234"]}]) f.write(json_argument) f.flush() - result = self.uri_param('event-name', p, 'file://%s' % f.name) + result = self.uri_param('event-name', p, 'file://%s' % f.name, mock.Mock()) self.assertEqual(result, json_argument) def test_uri_param_no_paramfile_true(self): @@ -100,7 +100,7 @@ def test_uri_param_no_paramfile_true(self): json_argument = json.dumps([{"Name": "instance-id", "Values": ["i-1234"]}]) f.write(json_argument) f.flush() - result = self.uri_param('event-name', p, 'file://%s' % f.name) + result = self.uri_param('event-name', p, 'file://%s' % f.name, mock.Mock()) self.assertEqual(result, None) diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index b720b0347986..b70920e2c2f9 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -91,7 +91,7 @@ "connect-timeout": { "type": "int", "help": "" - } + }, } }, } @@ -99,7 +99,8 @@ GET_VARIABLE = { 'provider': 'aws', 'output': 'json', - 'api_versions': {} + 'api_versions': {}, + 'cli_binary_format': 'raw-in-base64-out', } @@ -231,6 +232,9 @@ def get_config_variable(self, name): return GET_VARIABLE[name] return self.session_vars[name] + def get_scoped_config(self): + return GET_VARIABLE + def get_service_model(self, name, api_version=None): return botocore.model.ServiceModel( MINI_SERVICE, service_name='s3') @@ -539,6 +543,7 @@ def test_custom_arg_paramfile(self, mock_handler): param=mock.ANY, service_name='ec2', value='file:///foo', + parsed_globals=mock.ANY, ) # Make sure it was called with our passed-in URI self.assertEqual( @@ -568,6 +573,7 @@ def test_custom_command_paramfile(self, mock_handler): param=mock.ANY, service_name='custom', value='file:///foo', + parsed_globals=mock.ANY, ) def test_custom_arg_no_paramfile(self): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8030e2eba786..5d8f2710094d 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -23,7 +23,7 @@ split_on_commas, ignore_ctrl_c, find_service_and_method_in_event_name, is_document_type, is_document_type_container, is_streaming_blob_type, is_tagged_union_type, operation_uses_document_types, ShapeWalker, - ShapeRecordingVisitor, OutputStreamFactory + ShapeRecordingVisitor, OutputStreamFactory, resolve_v2_debug_mode ) @@ -135,6 +135,27 @@ def test_returns_none_if_event_is_too_short(self): self.assertIs(operation, None) +class TestV2DebugResolution(unittest.TestCase): + def test_v2_debug_flag_enabled(self): + args = mock.Mock(v2_debug=True) + self.assertTrue(resolve_v2_debug_mode(args)) + + def test_env_var_enabled(self): + args = mock.Mock(v2_debug=False) + with mock.patch.dict(os.environ, {'AWS_CLI_UPGRADE_DEBUG_MODE': 'true'}): + self.assertTrue(resolve_v2_debug_mode(args)) + + def test_all_disabled(self): + args = mock.Mock(v2_debug=False) + with mock.patch.dict(os.environ, {}, clear=True): + self.assertFalse(resolve_v2_debug_mode(args)) + + def test_env_var_non_true_value(self): + args = mock.Mock(v2_debug=False) + with mock.patch.dict(os.environ, {'AWS_CLI_UPGRADE_DEBUG_MODE': 'false'}): + self.assertFalse(resolve_v2_debug_mode(args)) + + class MockProcess(object): @property def stdin(self):