diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index ebd6b01aef5..8d5925bdf24 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -91,7 +91,8 @@ from ._constants import (FUNCTIONS_STACKS_API_KEYS, FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX, FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, PUBLIC_CLOUD, LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, - DOTNET_RUNTIME_NAME, NETCORE_RUNTIME_NAME, ASPDOTNET_RUNTIME_NAME, LINUX_OS_NAME, + DOTNET_RUNTIME_NAME, NETCORE_RUNTIME_NAME, ASPDOTNET_RUNTIME_NAME, NODE_RUNTIME_NAME, + LINUX_OS_NAME, WINDOWS_OS_NAME, LINUX_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, WINDOWS_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, DEFAULT_CENTAURI_IMAGE, VERSION_2022_09_01, FLEX_SUBNET_DELEGATION, @@ -8297,7 +8298,7 @@ def remove_delimiters(cls, runtime): return cls.DEFAULT_DELIMETER.join(filter(None, runtime)) def resolve(self, display_name, linux=False): - display_name = self.standardize_node_runtime_name(display_name).lower() + display_name = self.standardize_runtime_name(display_name).lower() stack = next((s for s in self.stacks if s.linux == linux and s.display_name.lower() == display_name), None) if stack is None: # help convert previously acceptable stack names into correct ones if runtime not found old_to_new_windows = { @@ -8393,6 +8394,25 @@ def standardize_node_runtime_name(runtime_name): return "NODE|{}".format(match.group(1)) return runtime_name + @staticmethod + def standardize_dotnet_runtime_name(runtime_name): + match = re.fullmatch(r'dotnet(?:core)?\|(\d+)(?:\.0)?', runtime_name, re.IGNORECASE) + if match and int(match.group(1)) >= 11: + return "dotnet|{}".format(match.group(1)) + return runtime_name + + @classmethod + def standardize_runtime_name(cls, runtime_name): + if not runtime_name: + return runtime_name + + runtime_family = runtime_name.split(cls.DEFAULT_DELIMETER, 1)[0].lower() + if runtime_family == NODE_RUNTIME_NAME: + return cls.standardize_node_runtime_name(runtime_name) + if runtime_family in (DOTNET_RUNTIME_NAME, NETCORE_RUNTIME_NAME): + return cls.standardize_dotnet_runtime_name(runtime_name) + return runtime_name + @classmethod def _is_valid_runtime_setting(cls, runtime_setting, include_eol=False): # Using datetime module imported at the top level @@ -8594,7 +8614,7 @@ def _parse_major_version_windows(self, major_version, parsed_results, config_map eol_date = self._format_eol_date(getattr(settings, 'end_of_life_date', None)) if "Java" not in minor_version.display_text: runtime_name = self._format_windows_display_text(minor_version.display_text) - runtime_name = self.standardize_node_runtime_name(runtime_name) + runtime_name = self.standardize_runtime_name(runtime_name) runtime = self.Runtime(display_name=runtime_name, linux=False, os="Windows", runtime_family=runtime_family, @@ -8706,7 +8726,7 @@ def _parse_major_version_linux(self, major_version, parsed_results, seen_runtime major_version, linux=True, java=False, include_eol=self._include_eol) for minor_version in minor_versions: settings = minor_version.stack_settings.linux_runtime_settings - runtime_name = self.standardize_node_runtime_name(settings.runtime_version) + runtime_name = self.standardize_runtime_name(settings.runtime_version) runtime = self.Runtime(display_name=runtime_name, configs={"linux_fx_version": runtime_name}, linux=True, @@ -12547,7 +12567,7 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None branch='master', login_with_github=False, force=False): runtime = _StackRuntimeHelper(cmd).remove_delimiters(runtime) # normalize "runtime:version" if runtime: - runtime = _StackRuntimeHelper.standardize_node_runtime_name(runtime) + runtime = _StackRuntimeHelper.standardize_runtime_name(runtime) if not token and not login_with_github: raise_missing_token_suggestion() elif not token: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 7cb3d748f72..6f3fe3716e2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -2319,6 +2319,81 @@ def __len__(self): return len(self._data) +class TestStackRuntimeDotnetLinux(unittest.TestCase): + + @staticmethod + def _new_helper(runtime_name): + from azure.cli.command_modules.appservice.custom import _StackRuntimeHelper + helper = _StackRuntimeHelper.__new__(_StackRuntimeHelper) + helper._include_eol = False + helper._stacks = [ + helper.Runtime( + display_name=runtime_name, + configs={'linux_fx_version': runtime_name}, + linux=True) + ] + return helper + + def test_resolve_legacy_dotnet_11_with_canonical_catalog(self): + helper = self._new_helper('dotnet|11') + + runtime = helper.resolve('DOTNETCORE|11.0', linux=True) + + self.assertEqual(runtime.display_name, 'dotnet|11') + self.assertEqual(runtime.configs['linux_fx_version'], 'dotnet|11') + + def test_standardize_dotnet_runtime_name(self): + from azure.cli.command_modules.appservice.custom import _StackRuntimeHelper + test_cases = { + 'DOTNETCORE|11.0': 'dotnet|11', + 'dotnet|11': 'dotnet|11', + 'DOTNET|11.0': 'dotnet|11', + 'DOTNETCORE|12.0': 'dotnet|12', + 'DOTNETCORE|10.0': 'DOTNETCORE|10.0', + } + + for runtime_name, expected in test_cases.items(): + with self.subTest(runtime_name=runtime_name): + self.assertEqual( + _StackRuntimeHelper.standardize_dotnet_runtime_name(runtime_name), + expected) + + def test_standardize_runtime_name_selects_dotnet_standardizer(self): + from azure.cli.command_modules.appservice.custom import _StackRuntimeHelper + + with mock.patch.object( + _StackRuntimeHelper, + 'standardize_node_runtime_name', + wraps=_StackRuntimeHelper.standardize_node_runtime_name) as node_standardizer: + self.assertEqual( + _StackRuntimeHelper.standardize_runtime_name('DOTNETCORE|11.0'), + 'dotnet|11') + + node_standardizer.assert_not_called() + + def test_parse_legacy_dotnet_11_catalog_as_canonical(self): + helper = self._new_helper('placeholder') + parsed_results = [] + github_settings = types.SimpleNamespace(is_supported=True, supported_version='11.x') + settings = types.SimpleNamespace( + runtime_version='DOTNETCORE|11.0', + end_of_life_date=None, + git_hub_action_settings=github_settings) + minor_version = types.SimpleNamespace( + display_text='.NET 11 (STS)', + stack_settings=types.SimpleNamespace(linux_runtime_settings=settings)) + + with mock.patch.object(helper, '_get_valid_minor_versions', side_effect=[[], [minor_version]]): + helper._parse_major_version_linux( + types.SimpleNamespace(display_text='.NET 11'), + parsed_results, + set(), + runtime_family='.NET') + + self.assertEqual(parsed_results[0].display_name, 'dotnet|11') + self.assertEqual(parsed_results[0].configs['linux_fx_version'], 'dotnet|11') + + class TestStackRuntimeNodeStandardization(unittest.TestCase): @staticmethod def _new_helper():