Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ARM] Fix various issues with az bicep #31041

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
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
42 changes: 31 additions & 11 deletions src/azure-cli/azure/cli/command_modules/resource/_bicep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?: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<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" # pylint: disable=line-too-long

Expand All @@ -48,7 +55,6 @@

_requests_verify = not should_disable_connection_verify()


def validate_bicep_target_scope(template_schema, deployment_scope):
target_scope = _template_schema_to_target_scope(template_schema)
if target_scope != deployment_scope:
Expand Down Expand Up @@ -78,7 +84,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:
Expand Down Expand Up @@ -108,6 +114,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)
Expand Down Expand Up @@ -145,10 +161,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}".')
Expand All @@ -170,10 +186,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):
Expand Down Expand Up @@ -223,7 +239,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)

Expand Down Expand Up @@ -275,26 +291,30 @@ 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
target_platform = "linux-musl-x64"
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
Expand Down Expand Up @@ -374,4 +394,4 @@ def _template_schema_to_target_scope(template_schema):
return "managementGroup"
if template_type_lower == "tenantdeployment":
return "tenant"
return None
return None
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# --------------------------------------------------------------------------------------------
# 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -61,40 +79,50 @@ 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)

debug_mock.assert_called_with(
"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")
Expand All @@ -109,20 +137,23 @@ 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(
'A new Bicep release is available: %s. Upgrade now by running "az bicep upgrade".',
"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")
Expand Down Expand Up @@ -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".'
Expand Down Expand Up @@ -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")

Expand Down
Loading