From 57af7539fca97de5764b5adf32e6a44de21f2d33 Mon Sep 17 00:00:00 2001 From: Feiyue Yu Date: Mon, 19 Apr 2021 14:19:20 +0800 Subject: [PATCH] [Core] Linter rule for service_name.json (#3186) * service_name.py * run on modified extensions * import * test * restore blueprint * remove description * require extensionname * remove extensionname * update * resource mover * message * Update scripts/ci/service_name.py * Update scripts/ci/service_name.py --- scripts/ci/service_name.py | 127 ++++++++++++++++++++++++++++++++++++ scripts/ci/verify_linter.py | 9 +++ 2 files changed, 136 insertions(+) create mode 100644 scripts/ci/service_name.py diff --git a/scripts/ci/service_name.py b/scripts/ci/service_name.py new file mode 100644 index 00000000000..a0b6e16e7b9 --- /dev/null +++ b/scripts/ci/service_name.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +""" +Check format of service_name.json. Command and AzureServiceName are required. Others are optional. +Each highest level command group should have reference in service_name.json. +""" +import json + +from azure.cli.core import MainCommandsLoader, AzCli +from azure.cli.core._help import AzCliHelp, CliCommandHelpFile +from azure.cli.core.commands import AzCliCommandInvoker, ExtensionCommandSource +from azure.cli.core.parser import AzCliCommandParser +from knack.help import GroupHelpFile + + +def get_extension_help_files(cli_ctx): + + # 1. Create invoker and load command table and arguments. Remember to turn off applicability check. + invoker = cli_ctx.invocation_cls(cli_ctx=cli_ctx, commands_loader_cls=cli_ctx.commands_loader_cls, + parser_cls=cli_ctx.parser_cls, help_cls=cli_ctx.help_cls) + cli_ctx.invocation = invoker + + invoker.commands_loader.skip_applicability = True + cmd_table = invoker.commands_loader.load_command_table(None) + + # turn off applicability check for all loaders + for loaders in invoker.commands_loader.cmd_to_loader_map.values(): + for loader in loaders: + loader.skip_applicability = True + + # filter the command table to only get commands from extensions + cmd_table = {k: v for k, v in cmd_table.items() if isinstance(v.command_source, ExtensionCommandSource)} + invoker.commands_loader.command_table = cmd_table + print('FOUND {} command(s) from the extension.'.format(len(cmd_table))) + + for cmd_name in cmd_table: + invoker.commands_loader.load_arguments(cmd_name) + + invoker.parser.load_command_table(invoker.commands_loader) + + # 2. Now load applicable help files + parser_keys = [] + parser_values = [] + sub_parser_keys = [] + sub_parser_values = [] + _store_parsers(invoker.parser, parser_keys, parser_values, sub_parser_keys, sub_parser_values) + for cmd, parser in zip(parser_keys, parser_values): + if cmd not in sub_parser_keys: + sub_parser_keys.append(cmd) + sub_parser_values.append(parser) + help_ctx = cli_ctx.help_cls(cli_ctx=cli_ctx) + help_files = [] + for cmd, parser in zip(sub_parser_keys, sub_parser_values): + try: + help_file = GroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) \ + else CliCommandHelpFile(help_ctx, cmd, parser) + help_file.load(parser) + help_files.append(help_file) + except Exception as ex: + print("Skipped '{}' due to '{}'".format(cmd, ex)) + help_files = sorted(help_files, key=lambda x: x.command) + return help_files + + +def _store_parsers(parser, parser_keys, parser_values, sub_parser_keys, sub_parser_values): + for s in parser.subparsers.values(): + parser_keys.append(_get_parser_name(s)) + parser_values.append(s) + if _is_group(s): + for c in s.choices.values(): + sub_parser_keys.append(_get_parser_name(c)) + sub_parser_values.append(c) + _store_parsers(c, parser_keys, parser_values, sub_parser_keys, sub_parser_values) + + +def _get_parser_name(s): + return (s._prog_prefix if hasattr(s, '_prog_prefix') else s.prog)[3:] + + +def _is_group(parser): + return getattr(parser, '_subparsers', None) is not None \ + or getattr(parser, 'choices', None) is not None + + +def check(): + az_cli = AzCli(cli_name='az', + commands_loader_cls=MainCommandsLoader, + invocation_cls=AzCliCommandInvoker, + parser_cls=AzCliCommandParser, + help_cls=AzCliHelp) + help_files = get_extension_help_files(az_cli) + # High command represents left most word in a command, e.g., vm, disk. + high_command_set = set() + for help_file in help_files: + if help_file.command: + high_command_set.add(help_file.command.split()[0]) + print('high_command_set:') + print(high_command_set) + + # Load and check service_name.json + with open('src/service_name.json') as f: + service_names = json.load(f) + print('Verifying src/service_name.json') + service_name_map = {} + for service_name in service_names: + command = service_name['Command'] + service = service_name['AzureServiceName'] + if not command.startswith('az '): + raise Exception('{} does not start with az!'.format(command)) + if not service: + raise Exception('AzureServiceName of {} is empty!'.format(command)) + service_name_map[command[3:]] = service + print('service_name_map:') + print(service_name_map) + + # Check existence in service_name.json + for high_command in high_command_set: + if high_command not in service_name_map: + raise Exception('No entry of {} in service_name.json. Please add one to the file.'.format(high_command)) + + +if __name__ == "__main__": + check() diff --git a/scripts/ci/verify_linter.py b/scripts/ci/verify_linter.py index 02db0d9d4a1..7fee6294307 100644 --- a/scripts/ci/verify_linter.py +++ b/scripts/ci/verify_linter.py @@ -15,6 +15,8 @@ from subprocess import check_output, check_call from pkg_resources import parse_version +import service_name + def separator_line(): print('-' * 100) @@ -160,6 +162,9 @@ def linter_on_external_extension(index_json): azdev_extension = AzdevExtensionHelper(name) azdev_extension.linter() + print('Checking service name for external extensions') + service_name.check() + az_extension.remove() @@ -181,6 +186,10 @@ def linter_on_internal_extension(modified_files): azdev_extension = AzdevExtensionHelper(name) azdev_extension.add_from_code() azdev_extension.linter() + + print('Checking service name for internal extensions') + service_name.check() + azdev_extension.remove()