Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/azure-cli-core/azure/cli/core/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def __init__(self, name, path=None):
super().__init__(name, 'whl', path)

def get_version(self):
return self.metadata.get('version')
return self.metadata.get('version') if self.metadata else None

def get_metadata(self):
from glob import glob
Expand Down Expand Up @@ -210,7 +210,7 @@ def __init__(self, name, path):
super().__init__(name, 'dev', path)

def get_version(self):
return self.metadata.get('version')
return self.metadata.get('version') if self.metadata else None

def get_metadata(self):
import pkginfo
Expand Down
5 changes: 4 additions & 1 deletion src/azure-cli-core/azure/cli/core/extension/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ def update_extension(cmd=None, extension_name=None, index_url=None, pip_extra_in
raise CLIError(
f"Cannot update system extension {extension_name}, please wait until Cloud Shell updates it in the next release.")
cur_version = ext.get_version()
if cur_version is None:
logger.warning("The '%s' extension has missing or invalid metadata. It may have been partially installed. "
"Proceeding with update to attempt to repair it.", extension_name)
try:
if not download_url:
download_url, ext_sha256 = resolve_from_index(extension_name, cur_version=cur_version, index_url=index_url, target_version=version, cli_ctx=cmd_cli_ctx, allow_preview=allow_preview)
Expand Down Expand Up @@ -435,7 +438,7 @@ def update_extension(cmd=None, extension_name=None, index_url=None, pip_extra_in
logger.error(err)
logger.debug('Copying %s to %s', backup_dir, extension_path)
shutil.copytree(backup_dir, extension_path)
raise CLIError('Failed to update. Rolled {} back to {}.'.format(extension_name, cur_version))
raise CLIError('Failed to update. Rolled {} back to {}.'.format(extension_name, cur_version or 'unknown'))
CommandIndex().invalidate()
except ExtensionNotInstalledException as e:
raise CLIError(e)
Expand Down
37 changes: 37 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,37 @@ def test_update_extension_with_preview(self):
self.assertEqual(ext['name'], extension_name)
self.assertEqual(ext['version'], '1.4.1a1')

def test_update_extension_with_corrupted_metadata(self):
"""Test that update_extension works when extension has missing/corrupted metadata (partial install)."""
extension_name = "extension-test-pkg"
extension2 = 'extension_test_pkg-1.2.3-py3-none-any.whl'

mocked_index_data = {
extension_name: [
mock_ext(extension2, version='1.2.3', download_url=_get_test_data_file(extension2)),
]
}
with mock.patch('azure.cli.core.extension._resolve.get_index_extensions',
return_value=mocked_index_data):
add_extension(self.cmd, extension_name=extension_name)
ext = show_extension(extension_name)
self.assertEqual(ext['name'], extension_name)
self.assertEqual(ext['version'], '1.2.3')

# Simulate corrupted/partial install by patching get_version to return None
with mock.patch('azure.cli.core.extension.operations.logger') as mock_logger:
with mock.patch.object(WheelExtension, 'get_version', return_value=None):
update_extension(self.cmd, extension_name)

# Verify the warning about missing metadata was emitted
warning_messages = [str(call) for call in mock_logger.warning.call_args_list]
self.assertTrue(any(extension_name in msg for msg in warning_messages))

# Verify the extension was successfully repaired/updated
ext = show_extension(extension_name)
self.assertEqual(ext['name'], extension_name)
self.assertEqual(ext['version'], '1.2.3')

@mock.patch('sys.stdin.isatty', return_value=True)
def test_ext_dynamic_install_config_tty(self, _):
from azure.cli.core.extension.dynamic_install import _get_extension_use_dynamic_install_config
Expand Down Expand Up @@ -497,6 +528,12 @@ def test_wheel_metadata2(self):
# We check that we can retrieve any one of the az extension metadata values
self.assertTrue(ext.metadata.get(EXT_METADATA_MINCLICOREVERSION))

def test_wheel_get_version_with_none_metadata(self):
"""Test that get_version() returns None when metadata is None (e.g. corrupted/partial installation)."""
ext = WheelExtension(EXT_NAME)
with mock.patch.object(type(ext), 'metadata', new_callable=mock.PropertyMock, return_value=None):
self.assertIsNone(ext.get_version())


class TestWheelSystemExtension(TestExtensionsBase):

Expand Down
Loading