diff --git a/src/azure-cli/azure/cli/command_modules/resource/_bicep.py b/src/azure-cli/azure/cli/command_modules/resource/_bicep.py index 2840e0bcc2c..bbea23a7908 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_bicep.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_bicep.py @@ -30,6 +30,13 @@ ) from azure.cli.core.util import should_disable_connection_verify +from ._bicep_config import ( + get_check_version_config, + get_use_binary_from_path_config, + remove_use_binary_from_path_config, + set_use_binary_from_path_config +) + # See: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string _semver_pattern = r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" # pylint: disable=line-too-long @@ -78,7 +85,7 @@ def run_bicep_command(cli_ctx, args, auto_install=True, custom_env=None): installed = os.path.isfile(installation_path) _logger.debug("Bicep CLI installed: %s.", installed) - check_version = cli_ctx.config.getboolean("bicep", "check_version", True) + check_version = get_check_version_config(cli_ctx) if not installed: if auto_install: @@ -108,6 +115,16 @@ def run_bicep_command(cli_ctx, args, auto_install=True, custom_env=None): def ensure_bicep_installation(cli_ctx, release_tag=None, target_platform=None, stdout=True): + if _use_binary_from_path(cli_ctx): + from shutil import which + + if which("bicep") is None: + raise ValidationError( + 'Could not find the "bicep" executable on PATH. To install Bicep via Azure CLI, set the "bicep.use_binary_from_path" configuration to False and run "az bicep install".' # pylint: disable=line-too-long + ) + + return + system = platform.system() machine = platform.machine() installation_path = _get_bicep_installation_path(system) @@ -145,10 +162,10 @@ def ensure_bicep_installation(cli_ctx, release_tag=None, target_platform=None, s os.chmod(installation_path, os.stat(installation_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - use_binary_from_path = cli_ctx.config.get("bicep", "use_binary_from_path", "if_found_in_ci").lower() + use_binary_from_path = get_use_binary_from_path_config(cli_ctx) if use_binary_from_path not in ["0", "no", "false", "off"]: _logger.warning("The configuration value of bicep.use_binary_from_path has been set to 'false'.") - cli_ctx.config.set_value("bicep", "use_binary_from_path", "false") + set_use_binary_from_path_config(cli_ctx, "false") if stdout: print(f'Successfully installed Bicep CLI to "{installation_path}".') @@ -170,10 +187,10 @@ def remove_bicep_installation(cli_ctx): if os.path.exists(_bicep_version_check_file_path): os.remove(_bicep_version_check_file_path) - use_binary_from_path = cli_ctx.config.get("bicep", "use_binary_from_path", "if_found_in_ci").lower() + use_binary_from_path = get_use_binary_from_path_config(cli_ctx) if use_binary_from_path in ["0", "no", "false", "off"]: _logger.warning("The configuration value of bicep.use_binary_from_path has been reset") - cli_ctx.config.remove_option("bicep", "use_binary_from_path") + remove_use_binary_from_path_config(cli_ctx) def is_bicep_file(file_path): @@ -223,7 +240,7 @@ def _bicep_installed_in_ci(): def _use_binary_from_path(cli_ctx): - use_binary_from_path = cli_ctx.config.get("bicep", "use_binary_from_path", "if_found_in_ci").lower() + use_binary_from_path = get_use_binary_from_path_config(cli_ctx) _logger.debug('Current value of "use_binary_from_path": %s.', use_binary_from_path) @@ -275,18 +292,22 @@ def _get_bicep_installed_version(bicep_executable_path): return _extract_version(installed_version_output) +def _is_arm_architecture(machine): + return machine in ("arm64", "aarch64", "aarch64_be", "armv8b", "armv8l") + + def _has_musl_library_only(): return os.path.exists("/lib/ld-musl-x86_64.so.1") and not os.path.exists("/lib/x86_64-linux-gnu/libc.so.6") def _get_bicep_download_url(system, machine, release_tag, target_platform=None): if not target_platform: - if system == "Windows" and machine == "arm64": + if system == "Windows" and _is_arm_architecture(machine): target_platform = "win-arm64" elif system == "Windows": # default to x64 target_platform = "win-x64" - elif system == "Linux" and machine == "arm64": + elif system == "Linux" and _is_arm_architecture(machine): target_platform = "linux-arm64" elif system == "Linux" and _has_musl_library_only(): # check for alpine linux @@ -294,7 +315,7 @@ def _get_bicep_download_url(system, machine, release_tag, target_platform=None): elif system == "Linux": # default to x64 target_platform = "linux-x64" - elif system == "Darwin" and machine == "arm64": + elif system == "Darwin" and _is_arm_architecture(machine): target_platform = "osx-arm64" elif system == "Darwin": # default to x64 diff --git a/src/azure-cli/azure/cli/command_modules/resource/_bicep_config.py b/src/azure-cli/azure/cli/command_modules/resource/_bicep_config.py new file mode 100644 index 00000000000..a48db77b6e5 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/resource/_bicep_config.py @@ -0,0 +1,30 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +_config_section = "bicep" + +_use_binary_from_path_config_key = "use_binary_from_path" +_use_binary_from_path_config_default_value = "if_found_in_ci" + +_check_version_config_key = "check_version" +_check_version_config_default_value = True + + +def get_use_binary_from_path_config(cli_ctx): + return cli_ctx.config.get( + _config_section, _use_binary_from_path_config_key, _use_binary_from_path_config_default_value + ).lower() + + +def set_use_binary_from_path_config(cli_ctx, value): + cli_ctx.config.set_value(_config_section, _use_binary_from_path_config_key, value) + + +def remove_use_binary_from_path_config(cli_ctx): + cli_ctx.config.remove_option(_config_section, _use_binary_from_path_config_key) + + +def get_check_version_config(cli_ctx): + return cli_ctx.config.getboolean(_config_section, _check_version_config_key, _check_version_config_default_value) diff --git a/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_bicep.py b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_bicep.py index 49799ec539e..d3858ccf9b5 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_bicep.py +++ b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_bicep.py @@ -37,6 +37,10 @@ def test_run_bicep_command_raise_error_if_not_installed_and_not_auto_install(sel with self.assertRaisesRegex(CLIError, 'Bicep CLI not found. Install it now by running "az bicep install".'): run_bicep_command(self.cli_ctx, ["--version"], auto_install=False) + + @mock.patch("azure.cli.command_modules.resource._bicep._use_binary_from_path") + @mock.patch("azure.cli.command_modules.resource._bicep.set_use_binary_from_path_config") + @mock.patch("azure.cli.command_modules.resource._bicep.get_use_binary_from_path_config") @mock.patch("os.chmod") @mock.patch("os.stat") @mock.patch("io.BufferedWriter") @@ -45,7 +49,21 @@ def test_run_bicep_command_raise_error_if_not_installed_and_not_auto_install(sel @mock.patch("os.path.exists") @mock.patch("os.path.dirname") @mock.patch("os.path.isfile") - def test_use_bicep_cli_from_path_false_after_install(self, isfile_stub, dirname_stub, exists_stub, urlopen_stub, open_stub, buffered_writer_stub, stat_stub, chmod_stub): + def test_use_bicep_cli_from_path_false_after_install( + self, + isfile_stub, + dirname_stub, + exists_stub, + urlopen_stub, + open_stub, + buffered_writer_stub, + stat_stub, + chmod_stub, + get_use_binary_from_path_config_stub, + set_use_binary_from_path_config_mock, + user_binary_from_path_stub, + ): + # Arrange isfile_stub.return_value = False dirname_stub.return_value = "tmp" exists_stub.return_value = True @@ -61,32 +79,39 @@ def test_use_bicep_cli_from_path_false_after_install(self, isfile_stub, dirname_ response.getcode.return_value = 200 response.read.return_value = b"test" urlopen_stub.return_value = response + + user_binary_from_path_stub.return_value = False + get_use_binary_from_path_config_stub.return_value = "if_found_in_ci" + # Act ensure_bicep_installation(self.cli_ctx, release_tag="v0.14.85", stdout=False) - self.assertTrue(self.cli_ctx.config.get("bicep", "use_binary_from_path") == "false") + # Assert + set_use_binary_from_path_config_mock.assert_called_once_with(self.cli_ctx, "false") + + @mock.patch("azure.cli.command_modules.resource._bicep.get_use_binary_from_path_config") @mock.patch("shutil.which") - def test_run_bicep_command_raise_error_if_bicep_cli_not_found_when_use_binary_from_path_is_true(self, which_stub): + def test_run_bicep_command_raise_error_if_bicep_cli_not_found_when_use_binary_from_path_is_true(self, which_stub, get_use_binary_from_path_config_stub): which_stub.return_value = None - self.cli_ctx.config.set_value("bicep", "use_binary_from_path", "true") + get_use_binary_from_path_config_stub.return_value = "true" with self.assertRaisesRegex( CLIError, 'Could not find the "bicep" executable on PATH. To install Bicep via Azure CLI, set the "bicep.use_binary_from_path" configuration to False and run "az bicep install".', ): run_bicep_command(self.cli_ctx, ["--version"], auto_install=False) - - self.cli_ctx.config.set_value("bicep", "use_binary_from_path", "false") + @mock.patch.dict(os.environ, {"GITHUB_ACTIONS": "true"}, clear=True) + @mock.patch("azure.cli.command_modules.resource._bicep.get_use_binary_from_path_config") @mock.patch("azure.cli.command_modules.resource._bicep._logger.debug") @mock.patch("azure.cli.command_modules.resource._bicep._run_command") @mock.patch("shutil.which") - def test_run_bicep_command_use_bicep_cli_from_path_in_ci(self, which_stub, run_command_stub, debug_mock): + def test_run_bicep_command_use_bicep_cli_from_path_in_ci(self, which_stub, run_command_stub, debug_mock, get_use_binary_from_path_config_stub): which_stub.return_value = True run_command_stub.return_value = "Bicep CLI version 0.13.1 (e3ac80d678)" - self.cli_ctx.config.set_value("bicep", "use_binary_from_path", "if_found_in_ci") + get_use_binary_from_path_config_stub.return_value = "if_found_in_ci" run_bicep_command(self.cli_ctx, ["--version"], auto_install=False) @@ -94,7 +119,10 @@ def test_run_bicep_command_use_bicep_cli_from_path_in_ci(self, which_stub, run_c "Using Bicep CLI from PATH. %s", "Bicep CLI version 0.13.1 (e3ac80d678)", ) + + @mock.patch("azure.cli.command_modules.resource._bicep.get_check_version_config") + @mock.patch("azure.cli.command_modules.resource._bicep.get_use_binary_from_path_config") @mock.patch("azure.cli.command_modules.resource._bicep._logger.warning") @mock.patch("azure.cli.command_modules.resource._bicep._run_command") @mock.patch("azure.cli.command_modules.resource._bicep.ensure_bicep_installation") @@ -109,13 +137,15 @@ def test_run_bicep_command_check_version( ensure_bicep_installation_mock, _run_command_mock, warning_mock, + get_use_binary_from_path_config_stub, + get_check_version_config_stub, ): isfile_stub.return_value = True _get_bicep_installed_version_stub.return_value = semver.VersionInfo.parse("1.0.0") get_bicep_latest_release_tag_stub.return_value = "v2.0.0" + get_check_version_config_stub.return_value = "true" + get_use_binary_from_path_config_stub.return_value = "false" - self.cli_ctx.config.set_value("bicep", "check_version", "True") - self.cli_ctx.config.set_value("bicep", "use_binary_from_path", "false") run_bicep_command(self.cli_ctx, ["--version"]) warning_mock.assert_called_once_with( @@ -123,6 +153,7 @@ def test_run_bicep_command_check_version( "v2.0.0", ) + @mock.patch("azure.cli.command_modules.resource._bicep._logger.warning") @mock.patch("azure.cli.command_modules.resource._bicep._run_command") @mock.patch("azure.cli.command_modules.resource._bicep.ensure_bicep_installation") @@ -151,19 +182,37 @@ def test_run_bicep_command_check_version_cache_read_write( finally: self._remove_bicep_version_check_file() + + @mock.patch("azure.cli.command_modules.resource._bicep._use_binary_from_path") @mock.patch("os.path.isfile") @mock.patch("azure.cli.command_modules.resource._bicep._get_bicep_installed_version") @mock.patch("os.path.dirname") def test_ensure_bicep_installation_skip_download_if_installed_version_matches_release_tag( - self, dirname_mock, _get_bicep_installed_version_stub, isfile_stub + self, dirname_mock, _get_bicep_installed_version_stub, isfile_stub, user_binary_from_path_stub ): _get_bicep_installed_version_stub.return_value = semver.VersionInfo.parse("0.1.0") isfile_stub.return_value = True + user_binary_from_path_stub.return_value = False ensure_bicep_installation(self.cli_ctx, release_tag="v0.1.0") dirname_mock.assert_not_called() + + @mock.patch("azure.cli.command_modules.resource._bicep.get_use_binary_from_path_config") + @mock.patch("azure.cli.command_modules.resource._bicep._get_bicep_installation_path") + @mock.patch("shutil.which") + def test_ensure_bicep_installation_skip_download_if_use_binary_from_path_is_true( + self, which_stub, _get_bicep_installation_path_mock, get_use_binary_from_path_config_stub + ): + which_stub.return_value = True + get_use_binary_from_path_config_stub.return_value = "true" + + ensure_bicep_installation(self.cli_ctx, release_tag="v0.1.0") + + _get_bicep_installation_path_mock.assert_not_called() + + def test_validate_target_scope_raise_error_if_target_scope_does_not_match_deployment_scope(self): with self.assertRaisesRegex( InvalidTemplateError, 'The target scope "tenant" does not match the deployment scope "subscription".' @@ -208,6 +257,9 @@ def test_get_bicep_download_url_returns_correct_urls(self): download_url = _get_bicep_download_url("Linux", "arm64", "v0.26.54") self.assertEqual(download_url, "https://downloads.bicep.azure.com/v0.26.54/bicep-linux-arm64") + download_url = _get_bicep_download_url("Linux", "aarch64", "v0.26.54") + self.assertEqual(download_url, "https://downloads.bicep.azure.com/v0.26.54/bicep-linux-arm64") + download_url = _get_bicep_download_url("Linux", "x64", "v0.26.54") self.assertEqual(download_url, "https://downloads.bicep.azure.com/v0.26.54/bicep-linux-x64")