From c4c554cff5b37b09ea97e85197645235d3314ab4 Mon Sep 17 00:00:00 2001 From: "khkh@microsoft.com" Date: Thu, 4 Apr 2024 12:05:12 -0500 Subject: [PATCH 1/3] Finished testing. --- .../cli/command_modules/appservice/_params.py | 4 + .../command_modules/appservice/commands.py | 1 + .../cli/command_modules/appservice/custom.py | 113 ++++++++++++------ 3 files changed, 84 insertions(+), 34 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 1a8e31495b7..92bede6b02f 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -177,6 +177,10 @@ def load_arguments(self, _): with self.argument_context('functionapp list-runtimes') as c: c.argument('os_type', options_list=["--os", "--os-type"], help="limit the output to just windows or linux runtimes", arg_type=get_enum_type([LINUX_OS_NAME, WINDOWS_OS_NAME])) + + with self.argument_context('functionapp list-flexconsumption-runtimes') as c: + c.argument('location', help="limit the output to just the runtimes available in the specified location") + c.argument('runtime', help="limit the output to just the specified runtime") with self.argument_context('webapp deleted list') as c: c.argument('name', arg_type=webapp_name_arg_type, id_part=None) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/commands.py b/src/azure-cli/azure/cli/command_modules/appservice/commands.py index 2ae27c36510..0530c96707a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -318,6 +318,7 @@ def load_command_table(self, _): g.custom_command('create', 'create_functionapp', exception_handler=ex_handler_factory(), validator=validate_functionapp) g.custom_command('list-runtimes', 'list_function_app_runtimes') + g.custom_command('list-flexconsumption-runtimes', 'list_flex_function_app_runtimes') g.custom_command('list', 'list_function_app', table_transformer=transform_web_list_output) g.custom_show_command('show', 'show_functionapp', table_transformer=transform_web_output) g.custom_command('delete', 'delete_function_app') 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 6f7651917c9..c91af89b9d8 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -415,14 +415,22 @@ def check_language_runtime(cmd, resource_group_name, name): client = web_client_factory(cmd.cli_ctx) app = client.web_apps.get(resource_group_name, name) is_linux = app.reserved + if is_functionapp(app): + is_flex = is_flex_functionapp(cmd.cli_ctx, resource_group_name, name) runtime_info = _get_functionapp_runtime_info(cmd, resource_group_name, name, None, is_linux) runtime = runtime_info['app_runtime'] runtime_version = runtime_info['app_runtime_version'] functions_version = runtime_info['functionapp_version'] - runtime_helper = _FunctionAppStackRuntimeHelper(cmd=cmd, linux=is_linux, windows=(not is_linux)) try: - runtime_helper.resolve(runtime, runtime_version, functions_version, is_linux) + if not is_flex: + runtime_helper = _FunctionAppStackRuntimeHelper(cmd=cmd, linux=is_linux, windows=(not is_linux)) + runtime_helper.resolve(runtime, runtime_version, functions_version, is_linux) + else: + location = app.location + logger.warning("Checking runtime stack for function app '%s' in location '%s'", name, location) + runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime, runtime_version) + runtime_helper.resolve(runtime, runtime_version) except ValidationError as e: logger.warning(e.error_msg) @@ -1375,6 +1383,10 @@ def list_function_app_runtimes(cmd, os_type=None): return windows_stacks return {WINDOWS_OS_NAME: windows_stacks, LINUX_OS_NAME: linux_stacks, 'flex': FLEX_RUNTIMES} +def list_flex_function_app_runtimes(cmd, location, runtime): + runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime) + return { 'flex': runtime_helper.stacks} + def delete_logic_app(cmd, resource_group_name, name, slot=None): return _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'delete', slot) @@ -1851,7 +1863,6 @@ def update_runtime_config(cmd, resource_group_name, name, runtime_version): runtime_info = _get_functionapp_runtime_info(cmd, resource_group_name, name, None, True) runtime = runtime_info['app_runtime'] - functionapp_version = runtime_info['functionapp_version'] runtimes = [r for r in FLEX_RUNTIMES if r['runtime'] == runtime] lang = next((r for r in runtimes if r['version'] == runtime_version), None) @@ -1860,9 +1871,13 @@ def update_runtime_config(cmd, resource_group_name, name, runtime_version): raise ValidationError("Invalid version {0} for runtime {1} for function apps on the " "Flex Consumption plan. Supported version for runtime {1} is {2}." .format(runtime_version, runtime, supported_versions)) - - runtime_helper = _FunctionAppStackRuntimeHelper(cmd, linux=True, windows=False) - matched_runtime = runtime_helper.resolve(runtime, runtime_version, functionapp_version, True) + + location = functionapp["location"] + logger.warning("location: %s", location) + runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime, runtime_version) + matched_runtime = runtime_helper.resolve(runtime, runtime_version) + logger.warning("matched_runtime: %s", matched_runtime) + logger.warning("matched_runtime.version: %s", matched_runtime.version) version = matched_runtime.version functionapp["properties"]["functionAppConfig"]["runtime"]["version"] = version @@ -3997,13 +4012,14 @@ def _load_stacks_hardcoded(self): class _FlexFunctionAppStackRuntimeHelper: class Runtime: - def __init__(self, name, version, app_insights=False, default=False, sku=None, end_of_life_date=None): + def __init__(self, name, version, app_insights=False, default=False, sku=None, end_of_life_date=None, runtime_info=None): self.name = name self.version = version self.app_insights = app_insights self.default = default self.sku = sku self.end_of_life_date = end_of_life_date + self.runtime_info = runtime_info def __init__(self, cmd, location, runtime, runtime_version=None): self._cmd = cmd @@ -4014,19 +4030,24 @@ def __init__(self, cmd, location, runtime, runtime_version=None): @property def stacks(self): + logger.warning("Getting function app stacks for runtime %s", self._runtime) self._load_stacks() return self._stacks def get_flex_raw_function_app_stacks(self, cmd, location, runtime): - # stacks_api_url = '/providers/Microsoft.Web/locations/{}/functionAppStacks?api-version=2020-10-01' - # '&removeHiddenStacks=true&removeDeprecatedStacks=true&stack={}' - # request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + stacks_api_url.format(location, runtime) - # response = send_raw_request(cmd.cli_ctx, "GET", request_url) - # return response.json()['value'] + stacks_api_url = '/providers/Microsoft.Web/locations/{}/functionAppStacks?api-version=2020-10-01&removeHiddenStacks=true&removeDeprecatedStacks=true&stack={}' + request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + stacks_api_url.format(location, runtime) + logger.warning("Request URL (Actual): %s", request_url) + + logger.warning("Getting function app stacks for runtime %s", runtime) + logger.warning("Request URL: %s", request_url) + + response = send_raw_request(cmd.cli_ctx, "GET", request_url) + return response.json()['value'] # temporary use of constant since the changes are not available yet - from ._constants import SAMPLE - return json.loads(SAMPLE)['value'] + # from ._constants import SAMPLE + # return json.loads(SAMPLE)['value'] def _parse_raw_stacks(self, stacks): runtime_to_version = {} @@ -4037,17 +4058,25 @@ def _parse_raw_stacks(self, stacks): runtime_settings = minor_version['stackSettings']['linuxRuntimeSettings'] runtime_name = (runtime_settings['appSettingsDictionary']['FUNCTIONS_WORKER_RUNTIME'] or runtime['name']) - if runtime_settings['Sku'] is None: + skus = runtime_settings['Sku'] + if skus is None: continue - runtime_version_properties = { - 'isDefault': runtime_settings.get('isDefault', False), - 'sku': runtime_settings['Sku'], - 'applicationInsights': runtime_settings['appInsightsSettings']['isSupported'], - 'endOfLifeDate': runtime_settings['endOfLifeDate'] - } - - runtime_to_version[runtime_name] = runtime_to_version.get(runtime_name, dict()) - runtime_to_version[runtime_name][runtime_version] = runtime_version_properties + + for sku in skus: + if sku['skuCode'] != 'FC1': + continue + + function_app_config = sku['functionAppConfigProperties'] + runtime_version_properties = { + 'isDefault': runtime_settings.get('isDefault', False), + 'sku': sku, + 'applicationInsights': runtime_settings['appInsightsSettings']['isSupported'], + 'endOfLifeDate': runtime_settings['endOfLifeDate'], + 'runtime_info': function_app_config + } + + runtime_to_version[runtime_name] = runtime_to_version.get(runtime_name, dict()) + runtime_to_version[runtime_name][runtime_version] = runtime_version_properties for runtime_name, versions in runtime_to_version.items(): for version_name, version_properties in versions.items(): @@ -4060,18 +4089,22 @@ def _create_runtime_from_properties(self, runtime_name, version_name, version_pr app_insights=version_properties['applicationInsights'], default=version_properties['isDefault'], sku=version_properties['sku'], - end_of_life_date=version_properties['endOfLifeDate']) + end_of_life_date=version_properties['endOfLifeDate'], + runtime_info=version_properties['runtime_info']) def _load_stacks(self): if self._stacks: return stacks = self.get_flex_raw_function_app_stacks(self._cmd, self._location, self._runtime) + logger.warning("\r\r\r\n\n\n\n Raw stacks: %s :EndOfMessage \r\r\r\r\n\n\n\n", stacks) self._parse_raw_stacks(stacks) def resolve(self, runtime, version=None): runtimes = self.stacks + logger.warning("Runtimes: %s", runtimes) + logger.debug("Runtimes: %s", runtimes) if not runtimes: - raise ValidationError("Runtime {} not supported for function apps on the Flex Consumption plan") + raise ValidationError("Runtime {} not supported for function apps on the Flex Consumption plan".format(runtime)) if version is None: return self.get_default_version() matched_runtime_version = next((r for r in runtimes if r.version == version), None) @@ -4663,9 +4696,10 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non elif flexconsumption_location: locations = list_flexconsumption_locations(cmd) + logger.debug("Flex consumption locations: %s", locations) location = next((loc for loc in locations if loc['name'].lower() == flexconsumption_location.lower()), None) - if location is None: - raise ValidationError("Location is invalid. Use: az functionapp list-flexconsumption-locations") + #if location is None: + # raise ValidationError("Location is invalid. Use: az functionapp list-flexconsumption-locations") is_linux = True # Following the same plan name format as the backend plan_name = "{}FlexPlan".format(name) @@ -4760,13 +4794,14 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non if flexconsumption_location: runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, flexconsumption_location, runtime, runtime_version) matched_runtime = runtime_helper.resolve(runtime, runtime_version) + logger.warning("Matched runtime: %s", json.dumps(matched_runtime.__dict__)) else: runtime_helper = _FunctionAppStackRuntimeHelper(cmd, linux=is_linux, windows=(not is_linux)) matched_runtime = runtime_helper.resolve("dotnet" if not runtime else runtime, runtime_version, functions_version, is_linux) if flexconsumption_location: - flex_sku = [s for s in matched_runtime.sku if s['skuCode'] == 'FC'][0] + flex_sku = matched_runtime.sku runtime = flex_sku['functionAppConfigProperties']['runtime']['name'] version = flex_sku['functionAppConfigProperties']['runtime']['version'] runtime_config = { @@ -4953,10 +4988,12 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non site_config.net_framework_version = None functionapp_def.reserved = None functionapp_def.is_xenon = None + functionapp_def.enable_additional_properties_sending() existing_properties = functionapp_def.serialize()["properties"] functionapp_def.additional_properties["properties"] = existing_properties functionapp_def.additional_properties["properties"]["functionAppConfig"] = function_app_config + functionapp_def.additional_properties["properties"]["SKU"] = "FlexConsumption" poller = client.web_apps.begin_create_or_update(resource_group_name, name, functionapp_def, api_version='2023-12-01') functionapp = LongRunningOperation(cmd.cli_ctx)(poller) @@ -7304,6 +7341,7 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None app = show_app(cmd, resource_group, name, slot) is_linux = app.reserved + # Verify github repo from github import Github, GithubException from github.GithubException import BadCredentialsException, UnknownObjectException @@ -7363,8 +7401,9 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None # Verify runtime + gh actions support functionapp_version = app_runtime_info['functionapp_version'] - github_actions_version = _get_functionapp_runtime_version(cmd=cmd, runtime_string=app_runtime_string, - runtime_version=github_actions_version, + location = app.location + github_actions_version = _get_functionapp_runtime_version(cmd=cmd, location=location, name=name, resource_group=resource_group, + runtime_string=app_runtime_string, runtime_version=github_actions_version, functionapp_version=functionapp_version, is_linux=is_linux) if not github_actions_version: @@ -7827,12 +7866,18 @@ def _runtime_supports_github_actions(cmd, runtime_string, is_linux): return False -def _get_functionapp_runtime_version(cmd, runtime_string, runtime_version, functionapp_version, is_linux): +def _get_functionapp_runtime_version(cmd, location, name, resource_group, runtime_string, runtime_version, functionapp_version, is_linux): runtime_version = re.sub(r"[^\d\.]", "", runtime_version).rstrip('.') matched_runtime = None - helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) + is_flex = is_flex_functionapp(cmd.cli_ctx, resource_group, name) + try: - matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) + if (not is_flex): + helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) + matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) + else: + runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime_string, runtime_version) + matched_runtime = runtime_helper.resolve(runtime_string, runtime_version) except ValidationError as e: if "Invalid version" in e.error_msg: index = e.error_msg.index("Run 'az functionapp list-runtimes' for more details on supported runtimes.") From 9c9d063d8147e357daa20fcae35b1ce7cd18126f Mon Sep 17 00:00:00 2001 From: "khkh@microsoft.com" Date: Thu, 4 Apr 2024 13:57:24 -0500 Subject: [PATCH 2/3] cleanup --- .../cli/command_modules/appservice/custom.py | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) 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 c91af89b9d8..1120c6e1ab1 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -428,7 +428,6 @@ def check_language_runtime(cmd, resource_group_name, name): runtime_helper.resolve(runtime, runtime_version, functions_version, is_linux) else: location = app.location - logger.warning("Checking runtime stack for function app '%s' in location '%s'", name, location) runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime, runtime_version) runtime_helper.resolve(runtime, runtime_version) except ValidationError as e: @@ -1862,8 +1861,8 @@ def update_runtime_config(cmd, resource_group_name, name, runtime_version): functionapp["properties"]["functionAppConfig"]["runtime"] = {} runtime_info = _get_functionapp_runtime_info(cmd, resource_group_name, name, None, True) + functionapp_version = runtime_info['functionapp_version'] runtime = runtime_info['app_runtime'] - runtimes = [r for r in FLEX_RUNTIMES if r['runtime'] == runtime] lang = next((r for r in runtimes if r['version'] == runtime_version), None) if lang is None: @@ -1873,11 +1872,8 @@ def update_runtime_config(cmd, resource_group_name, name, runtime_version): .format(runtime_version, runtime, supported_versions)) location = functionapp["location"] - logger.warning("location: %s", location) runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, location, runtime, runtime_version) matched_runtime = runtime_helper.resolve(runtime, runtime_version) - logger.warning("matched_runtime: %s", matched_runtime) - logger.warning("matched_runtime.version: %s", matched_runtime.version) version = matched_runtime.version functionapp["properties"]["functionAppConfig"]["runtime"]["version"] = version @@ -4030,25 +4026,15 @@ def __init__(self, cmd, location, runtime, runtime_version=None): @property def stacks(self): - logger.warning("Getting function app stacks for runtime %s", self._runtime) self._load_stacks() return self._stacks def get_flex_raw_function_app_stacks(self, cmd, location, runtime): stacks_api_url = '/providers/Microsoft.Web/locations/{}/functionAppStacks?api-version=2020-10-01&removeHiddenStacks=true&removeDeprecatedStacks=true&stack={}' request_url = cmd.cli_ctx.cloud.endpoints.resource_manager + stacks_api_url.format(location, runtime) - logger.warning("Request URL (Actual): %s", request_url) - - logger.warning("Getting function app stacks for runtime %s", runtime) - logger.warning("Request URL: %s", request_url) - response = send_raw_request(cmd.cli_ctx, "GET", request_url) return response.json()['value'] - # temporary use of constant since the changes are not available yet - # from ._constants import SAMPLE - # return json.loads(SAMPLE)['value'] - def _parse_raw_stacks(self, stacks): runtime_to_version = {} for runtime in stacks: @@ -4096,13 +4082,10 @@ def _load_stacks(self): if self._stacks: return stacks = self.get_flex_raw_function_app_stacks(self._cmd, self._location, self._runtime) - logger.warning("\r\r\r\n\n\n\n Raw stacks: %s :EndOfMessage \r\r\r\r\n\n\n\n", stacks) self._parse_raw_stacks(stacks) def resolve(self, runtime, version=None): runtimes = self.stacks - logger.warning("Runtimes: %s", runtimes) - logger.debug("Runtimes: %s", runtimes) if not runtimes: raise ValidationError("Runtime {} not supported for function apps on the Flex Consumption plan".format(runtime)) if version is None: @@ -4696,10 +4679,9 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non elif flexconsumption_location: locations = list_flexconsumption_locations(cmd) - logger.debug("Flex consumption locations: %s", locations) location = next((loc for loc in locations if loc['name'].lower() == flexconsumption_location.lower()), None) - #if location is None: - # raise ValidationError("Location is invalid. Use: az functionapp list-flexconsumption-locations") + if location is None: + raise ValidationError("Location is invalid. Use: az functionapp list-flexconsumption-locations") is_linux = True # Following the same plan name format as the backend plan_name = "{}FlexPlan".format(name) @@ -4794,7 +4776,6 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non if flexconsumption_location: runtime_helper = _FlexFunctionAppStackRuntimeHelper(cmd, flexconsumption_location, runtime, runtime_version) matched_runtime = runtime_helper.resolve(runtime, runtime_version) - logger.warning("Matched runtime: %s", json.dumps(matched_runtime.__dict__)) else: runtime_helper = _FunctionAppStackRuntimeHelper(cmd, linux=is_linux, windows=(not is_linux)) matched_runtime = runtime_helper.resolve("dotnet" if not runtime else runtime, @@ -4988,7 +4969,6 @@ def create_functionapp(cmd, resource_group_name, name, storage_account, plan=Non site_config.net_framework_version = None functionapp_def.reserved = None functionapp_def.is_xenon = None - functionapp_def.enable_additional_properties_sending() existing_properties = functionapp_def.serialize()["properties"] functionapp_def.additional_properties["properties"] = existing_properties From 5ae82b0b17e469a6e9a19261705501b1d37c0d10 Mon Sep 17 00:00:00 2001 From: "khkh@microsoft.com" Date: Thu, 4 Apr 2024 13:58:55 -0500 Subject: [PATCH 3/3] Github actions command --- .../cli/command_modules/appservice/custom.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 1120c6e1ab1..57b8408e6f2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -4008,7 +4008,7 @@ def _load_stacks_hardcoded(self): class _FlexFunctionAppStackRuntimeHelper: class Runtime: - def __init__(self, name, version, app_insights=False, default=False, sku=None, end_of_life_date=None, runtime_info=None): + def __init__(self, name, version, app_insights=False, default=False, sku=None, end_of_life_date=None, runtime_info=None, github_actions_properties=None): self.name = name self.version = version self.app_insights = app_insights @@ -4016,6 +4016,12 @@ def __init__(self, name, version, app_insights=False, default=False, sku=None, e self.sku = sku self.end_of_life_date = end_of_life_date self.runtime_info = runtime_info + self.github_actions_properties = github_actions_properties + + class GithubActionsProperties: + def __init__(self, is_supported, supported_version): + self.is_supported = is_supported + self.supported_version = supported_version def __init__(self, cmd, location, runtime, runtime_version=None): self._cmd = cmd @@ -4045,6 +4051,7 @@ def _parse_raw_stacks(self, stacks): runtime_name = (runtime_settings['appSettingsDictionary']['FUNCTIONS_WORKER_RUNTIME'] or runtime['name']) skus = runtime_settings['Sku'] + github_actions_settings = runtime_settings['gitHubActionSettings'] if skus is None: continue @@ -4053,12 +4060,19 @@ def _parse_raw_stacks(self, stacks): continue function_app_config = sku['functionAppConfigProperties'] + + github_actions_properties = { + 'is_supported': github_actions_settings.get('isSupported', False), + 'supported_version': github_actions_settings['supportedVersion'], + } + runtime_version_properties = { 'isDefault': runtime_settings.get('isDefault', False), 'sku': sku, 'applicationInsights': runtime_settings['appInsightsSettings']['isSupported'], 'endOfLifeDate': runtime_settings['endOfLifeDate'], - 'runtime_info': function_app_config + 'runtime_info': function_app_config, + 'github_actions_properties': self.GithubActionsProperties(**github_actions_properties) } runtime_to_version[runtime_name] = runtime_to_version.get(runtime_name, dict()) @@ -4076,7 +4090,8 @@ def _create_runtime_from_properties(self, runtime_name, version_name, version_pr default=version_properties['isDefault'], sku=version_properties['sku'], end_of_life_date=version_properties['endOfLifeDate'], - runtime_info=version_properties['runtime_info']) + runtime_info=version_properties['runtime_info'], + github_actions_properties=version_properties['github_actions_properties']) def _load_stacks(self): if self._stacks: @@ -7866,6 +7881,7 @@ def _get_functionapp_runtime_version(cmd, location, name, resource_group, runtim error_message += e.error_msg[index:].lower() raise ValidationError(error_message) raise e + if not matched_runtime: return None if matched_runtime.github_actions_properties: