diff --git a/doc/extensions/metadata.md b/doc/extensions/metadata.md index 8a6c0f624..b8101a71c 100644 --- a/doc/extensions/metadata.md +++ b/doc/extensions/metadata.md @@ -50,4 +50,9 @@ Type: `string` Example: `"azext.maxCliCoreVersion": "2.0.15"` +### azext.isPreview +Description: Indicate that the extension is in preview. +Type: `boolean` + +Example: `"azext.isPreview": true` diff --git a/src/azure-cli-core/HISTORY.rst b/src/azure-cli-core/HISTORY.rst index 3e2ee4727..d5d6fa091 100644 --- a/src/azure-cli-core/HISTORY.rst +++ b/src/azure-cli-core/HISTORY.rst @@ -6,7 +6,7 @@ Release History 2.0.30 ++++++ -* Minor fixes +* Show message for extensions marked as preview on -h. 2.0.29 ++++++ diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 098feb8f4..e510f4042 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -163,7 +163,8 @@ class MainCommandsLoader(CLICommandsLoader): if extensions: logger.debug("Found %s extensions: %s", len(extensions), [e.name for e in extensions]) allowed_extensions = _handle_extension_suppressions(extensions) - for ext_name in [e.name for e in allowed_extensions]: + for ext in allowed_extensions: + ext_name = ext.name ext_dir = get_extension_path(ext_name) sys.path.append(ext_dir) try: @@ -177,7 +178,8 @@ class MainCommandsLoader(CLICommandsLoader): for cmd_name, cmd in extension_command_table.items(): cmd.command_source = ExtensionCommandSource( extension_name=ext_name, - overrides_command=cmd_name in cmd_to_mod_map) + overrides_command=cmd_name in cmd_to_mod_map, + preview=ext.preview) self.command_table.update(extension_command_table) elapsed_time = timeit.default_timer() - start_time diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index f64a8ea7f..a6fad47aa 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -56,6 +56,8 @@ class AzCliHelp(CLIHelp): return if help_file.command_source and isinstance(help_file.command_source, ExtensionCommandSource): logger.warning(help_file.command_source.get_command_warn_msg()) + if help_file.command_source.preview: + logger.warning(help_file.command_source.get_preview_warn_msg()) @classmethod def print_detailed_help(cls, cli_name, help_file): diff --git a/src/azure-cli-core/azure/cli/core/commands/__init__.py b/src/azure-cli-core/azure/cli/core/commands/__init__.py index cb85cb813..1eeae48ab 100644 --- a/src/azure-cli-core/azure/cli/core/commands/__init__.py +++ b/src/azure-cli-core/azure/cli/core/commands/__init__.py @@ -568,11 +568,12 @@ def _load_module_command_loader(loader, args, mod): class ExtensionCommandSource(object): """ Class for commands contributed by an extension """ - def __init__(self, overrides_command=False, extension_name=None): + def __init__(self, overrides_command=False, extension_name=None, preview=False): super(ExtensionCommandSource, self).__init__() # True if the command overrides a CLI command self.overrides_command = overrides_command self.extension_name = extension_name + self.preview = preview def get_command_warn_msg(self): if self.overrides_command: @@ -585,6 +586,11 @@ class ExtensionCommandSource(object): return "This command is from the following extension: {}".format(self.extension_name) return "This command is from an extension." + def get_preview_warn_msg(self): + if self.preview: + return "The extension is in preview" + return None + def _load_client_exception_class(): # Since loading msrest is expensive, we avoid it until we have to diff --git a/src/azure-cli-core/azure/cli/core/extension.py b/src/azure-cli-core/azure/cli/core/extension.py index 460fba21d..538af3516 100644 --- a/src/azure-cli-core/azure/cli/core/extension.py +++ b/src/azure-cli-core/azure/cli/core/extension.py @@ -22,6 +22,7 @@ AZEXT_METADATA_FILENAME = 'azext_metadata.json' EXT_METADATA_MINCLICOREVERSION = 'azext.minCliCoreVersion' EXT_METADATA_MAXCLICOREVERSION = 'azext.maxCliCoreVersion' +EXT_METADATA_ISPREVIEW = 'azext.isPreview' logger = get_logger(__name__) @@ -42,6 +43,7 @@ class Extension(object): self.ext_type = ext_type self._version = None self._metadata = None + self._preview = None @property def version(self): @@ -67,6 +69,19 @@ class Extension(object): logger.debug("Unable to get extension metadata: %s", traceback.format_exc()) return self._metadata + @property + def preview(self): + """ + Lazy load preview status. + Returns the preview status of the extension. + """ + try: + if not isinstance(self._preview, bool): + self._preview = bool(self.metadata.get(EXT_METADATA_ISPREVIEW)) + except Exception: # pylint: disable=broad-except + logger.debug("Unable to get extension preview status: %s", traceback.format_exc()) + return self._preview + def get_version(self): raise NotImplementedError() diff --git a/src/azure-cli-core/azure/cli/core/tests/test_command_registration.py b/src/azure-cli-core/azure/cli/core/tests/test_command_registration.py index cced5bf71..605299005 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_command_registration.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_command_registration.py @@ -152,9 +152,9 @@ class TestCommandRegistration(unittest.TestCase): return ext_name def _mock_get_extensions(): - MockExtension = namedtuple('Extension', ['name']) - return [MockExtension(name=__name__ + '.ExtCommandsLoader'), - MockExtension(name=__name__ + '.Ext2CommandsLoader')] + MockExtension = namedtuple('Extension', ['name', 'preview']) + return [MockExtension(name=__name__ + '.ExtCommandsLoader', preview=False), + MockExtension(name=__name__ + '.Ext2CommandsLoader', preview=False)] def _mock_load_command_loader(loader, args, name, prefix): diff --git a/src/command_modules/azure-cli-extension/HISTORY.rst b/src/command_modules/azure-cli-extension/HISTORY.rst index ef5ba6189..b69ea770a 100644 --- a/src/command_modules/azure-cli-extension/HISTORY.rst +++ b/src/command_modules/azure-cli-extension/HISTORY.rst @@ -5,7 +5,9 @@ Release History 0.0.11 ++++++ -* Minor fixes +* Preview extensions: Show message on `az extension add` if extension is in preview +* BC: `az extension list-available` - The full extension data is now available with `--show-details` +* `az extension list-available` - A simplified view of the extensions available is now shown by default 0.0.10 +++++++ diff --git a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/__init__.py b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/__init__.py index 1e6e71651..dd83874fd 100644 --- a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/__init__.py +++ b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/__init__.py @@ -27,7 +27,10 @@ class ExtensionCommandsLoader(AzCommandsLoader): return bool(not command_args.get('source') or prompt_y_n('Are you sure you want to install this extension?')) def transform_extension_list_available(results): - return [OrderedDict([('Name', r)]) for r in results] + if isinstance(results, dict): + # For --show-details, transform the table + return [OrderedDict([('Name', r)]) for r in results] + return results def validate_extension_add(namespace): if (namespace.extension_name and namespace.source) or (not namespace.extension_name and not namespace.source): @@ -67,5 +70,8 @@ class ExtensionCommandsLoader(AzCommandsLoader): c.argument('source', options_list=['--source', '-s'], help='Filepath or URL to an extension', completer=FilesCompleter()) c.argument('yes', options_list=['--yes', '-y'], action='store_true', help='Do not prompt for confirmation.') + with self.argument_context('extension list-available') as c: + c.argument('show_details', options_list=['--show-details', '-d'], action='store_true', help='Show the raw data from the extension index.') + COMMAND_LOADER_CLS = ExtensionCommandsLoader diff --git a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/custom.py b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/custom.py index ea897338d..9ba515f06 100644 --- a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/custom.py +++ b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/custom.py @@ -11,14 +11,17 @@ import traceback import hashlib from subprocess import check_output, STDOUT, CalledProcessError from six.moves.urllib.parse import urlparse # pylint: disable=import-error +from collections import OrderedDict import requests from wheel.install import WHEEL_INFO_RE +from pkg_resources import parse_version + from knack.log import get_logger from azure.cli.core.util import CLIError from azure.cli.core.extension import (extension_exists, get_extension_path, get_extensions, - get_extension, ext_compat_with_cli, + get_extension, ext_compat_with_cli, EXT_METADATA_ISPREVIEW, WheelExtension, ExtensionNotInstalledException) from azure.cli.core.telemetry import set_extension_management_detail @@ -200,6 +203,11 @@ def add_extension(source=None, extension_name=None, index_url=None, yes=None, # raise CLIError("No matching extensions for '{}'. Use --debug for more information.".format(extension_name)) _add_whl_ext(source, ext_sha256=ext_sha256, pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy) _augment_telemetry_with_ext_info(extension_name) + try: + if extension_name and get_extension(extension_name).preview: + logger.warning("The installed extension '%s' is in preview.", extension_name) + except ExtensionNotInstalledException: + pass def remove_extension(extension_name): @@ -263,8 +271,28 @@ def update_extension(extension_name, index_url=None, pip_extra_index_urls=None, raise CLIError(e) -def list_available_extensions(index_url=None): - return get_index_extensions(index_url=index_url) +def list_available_extensions(index_url=None, show_details=False): + index_data = get_index_extensions(index_url=index_url) + if show_details: + return index_data + installed_extensions = get_extensions() + installed_extension_names = [e.name for e in installed_extensions] + results = [] + for name, items in OrderedDict(sorted(index_data.items())).items(): + latest = sorted(items, key=lambda c: parse_version(c['metadata']['version']), reverse=True)[0] + installed = False + if name in installed_extension_names: + installed = True + if parse_version(latest['metadata']['version']) > parse_version(get_extension(name).version): + installed = str(True) + ' (upgrade available)' + results.append({ + 'name': name, + 'version': latest['metadata']['version'], + 'summary': latest['metadata']['summary'], + 'preview': latest['metadata'].get(EXT_METADATA_ISPREVIEW, False), + 'installed': installed + }) + return results def get_lsb_release(): @@ -296,14 +324,14 @@ def check_distro_consistency(): except Exception as err: # pylint: disable=broad-except current_linux_dist_name = None stored_linux_dist_name = None - logger.debug('Linux distro check: An error occurred while checking \ -linux distribution version source list consistency.') + logger.debug('Linux distro check: An error occurred while checking ' + 'linux distribution version source list consistency.') logger.debug(err) if current_linux_dist_name != stored_linux_dist_name: - logger.warning("Linux distro check: Mismatch distribution \ -name in %s file", LIST_FILE_PATH) - logger.warning("Linux distro check: If command fails, install the appropriate package \ -for your distribution or change the above file accordingly.") + logger.warning("Linux distro check: Mismatch distribution " + "name in %s file", LIST_FILE_PATH) + logger.warning("Linux distro check: If command fails, install the appropriate package " + "for your distribution or change the above file accordingly.") logger.warning("Linux distro check: %s has '%s', current distro is '%s'", LIST_FILE_PATH, stored_linux_dist_name, current_linux_dist_name) diff --git a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/tests/latest/test_extension_commands.py b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/tests/latest/test_extension_commands.py index 7bf4e3647..2415ca6e9 100644 --- a/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/tests/latest/test_extension_commands.py +++ b/src/command_modules/azure-cli-extension/azure/cli/command_modules/extension/tests/latest/test_extension_commands.py @@ -251,6 +251,29 @@ class TestExtensionCommands(unittest.TestCase): list_available_extensions(index_url=index_url) c.assert_called_once_with(index_url) + def test_list_available_extensions_show_details(self): + with mock.patch('azure.cli.command_modules.extension.custom.get_index_extensions', autospec=True) as c: + list_available_extensions(show_details=True) + c.assert_called_once_with(None) + + def test_list_available_extensions_no_show_details(self): + sample_index_extensions = { + 'test_sample_extension1': [{ + 'metadata': { + 'name': 'test_sample_extension1', + 'summary': 'my summary', + 'version': '0.1.0' + }}] + } + with mock.patch('azure.cli.command_modules.extension.custom.get_index_extensions', return_value=sample_index_extensions): + res = list_available_extensions() + self.assertIsInstance(res, list) + self.assertEqual(len(res), len(sample_index_extensions)) + self.assertEqual(res[0]['name'], 'test_sample_extension1') + self.assertEqual(res[0]['summary'], 'my summary') + self.assertEqual(res[0]['version'], '0.1.0') + self.assertEqual(res[0]['preview'], False) + def test_add_list_show_remove_extension_extra_index_url(self): """ Tests extension addition while specifying --extra-index-url parameter.