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