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 a3b809afa62..f80449d9f07 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -7618,7 +7618,7 @@ def remove_delimiters(cls, runtime): return cls.DEFAULT_DELIMETER.join(filter(None, runtime)) def resolve(self, display_name, linux=False): - display_name = display_name.lower() + display_name = self._standardize_node_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 = { @@ -7707,6 +7707,13 @@ def _format_windows_display_text(cls, display_text): t = re.sub(r"\(.*\)", "", t) # remove "(LTS)" return t.replace(" ", "|", 1).replace(" ", "") + @staticmethod + def _standardize_node_runtime_name(runtime_name): + match = re.fullmatch(r'node\|(\d+)(?:-?lts)?', runtime_name, re.IGNORECASE) + if match and int(match.group(1)) >= 26: + return "NODE|{}".format(match.group(1)) + return runtime_name + @classmethod def _is_valid_runtime_setting(cls, runtime_setting, include_eol=False): # Using datetime module imported at the top level @@ -7908,6 +7915,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 = self.Runtime(display_name=runtime_name, linux=False, os="Windows", runtime_family=runtime_family, @@ -8019,9 +8027,9 @@ 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 = settings.runtime_version + runtime_name = self._standardize_node_runtime_name(settings.runtime_version) runtime = self.Runtime(display_name=runtime_name, - configs={"linux_fx_version": runtime_name}, + configs={"linux_fx_version": settings.runtime_version}, linux=True, os="Linux", runtime_family=runtime_family, 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 ec6a96e6502..419f1ea54d9 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 @@ -1643,6 +1643,74 @@ def __len__(self): return len(self._data) +class TestStackRuntimeNodeStandardization(unittest.TestCase): + @staticmethod + def _new_helper(): + from azure.cli.command_modules.appservice.custom import _StackRuntimeHelper + helper = _StackRuntimeHelper.__new__(_StackRuntimeHelper) + helper._linux = True + helper._windows = True + helper._include_eol = False + helper._stacks = [] + helper.windows_config_mappings = {'node': 'WEBSITE_NODE_DEFAULT_VERSION'} + return helper + + @staticmethod + def _node_stack(version, display_text, linux_runtime): + git_hub_action_settings = types.SimpleNamespace(is_supported=True, supported_version="{}.x".format(version)) + linux_settings = types.SimpleNamespace( + runtime_version=linux_runtime, + is_hidden=False, + is_deprecated=False, + end_of_life_date=None, + git_hub_action_settings=git_hub_action_settings, + ) + windows_settings = types.SimpleNamespace( + runtime_version="~{}".format(version), + is_hidden=False, + is_deprecated=False, + end_of_life_date=None, + git_hub_action_settings=git_hub_action_settings, + ) + minor = types.SimpleNamespace( + display_text=display_text, + stack_settings=types.SimpleNamespace( + linux_container_settings=None, + linux_runtime_settings=linux_settings, + windows_container_settings=None, + windows_runtime_settings=windows_settings, + ), + ) + major = types.SimpleNamespace(display_text=display_text, minor_versions=[minor]) + return types.SimpleNamespace(display_text='Node', major_versions=[major]) + + def test_node_26_uses_standard_identifier_on_both_platforms(self): + helper = self._new_helper() + helper._parse_raw_stacks([self._node_stack('26', 'Node 26 LTS', 'NODE|26-lts')]) + + self.assertEqual( + [(runtime.os, runtime.display_name) for runtime in helper._stacks], + [('Linux', 'NODE|26'), ('Windows', 'NODE|26')]) + self.assertEqual( + [(row['os'], row['config']) for row in helper.get_stacks_as_table(runtime_filter='node')], + [('Linux', 'NODE|26'), ('Windows', 'NODE|26')]) + self.assertEqual(helper.resolve('NODE|26', linux=True).configs['linux_fx_version'], 'NODE|26-lts') + self.assertEqual( + helper.resolve('NODE|26', linux=False).configs['WEBSITE_NODE_DEFAULT_VERSION'], '~26') + self.assertEqual( + helper.resolve('NODE|26-lts', linux=True).configs['linux_fx_version'], 'NODE|26-lts') + self.assertEqual( + helper.resolve('NODE|26LTS', linux=False).configs['WEBSITE_NODE_DEFAULT_VERSION'], '~26') + + def test_older_node_identifiers_remain_unchanged(self): + helper = self._new_helper() + helper._parse_raw_stacks([self._node_stack('24', 'Node 24 LTS', 'NODE|24-lts')]) + + self.assertEqual( + [(runtime.os, runtime.display_name) for runtime in helper._stacks], + [('Linux', 'NODE|24-lts'), ('Windows', 'NODE|24LTS')]) + + class TestStackRuntimeJavaSELinux(unittest.TestCase): """Regression tests for `az webapp list-runtimes` Linux Java SE parsing.