From 7e4d071a4339992aaea92709413e34f1a63171ea Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Mon, 18 Jul 2022 11:27:59 -0700 Subject: [PATCH 01/15] WIP --- .../command_modules/appservice/_constants.py | 14 + .../command_modules/appservice/commands.py | 4 + .../cli/command_modules/appservice/custom.py | 307 +++++++++++++++++- 3 files changed, 324 insertions(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py index 424cd3a69ea..247d36084a7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -75,3 +75,17 @@ def __init__(self): 'java': 'AppService/windows/java-jar-webapp-on-azure.yml', 'tomcat': 'AppService/windows/java-war-webapp-on-azure.yml' } + +LINUX_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { + 'node': 'FunctionApp/linux-node.js-functionapp-on-azure.yml', + 'python': 'FunctionApp/linux-python-functionapp-on-azure.yml', + 'dotnet': 'FunctionApp/linux-dotnet-functionapp-on-azure.yml', + 'java': 'FunctionApp/linux-java-functionapp-on-azure.yml', +} + +WINDOWS_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { + 'node': 'FunctionApp/windows-node.js-functionapp-on-azure.yml', + 'dotnet': 'FunctionApp/windows-dotnet-functionapp-on-azure.yml', + 'java': 'FunctionApp/windows-java-functionapp-on-azure.yml', + 'powershell': 'FunctionApp/windows-powershell-functionapp-on-azure.yml', +} 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 038040ef33e..3716a5166f3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -259,6 +259,10 @@ def load_command_table(self, _): g.custom_command('add', 'add_github_actions') g.custom_command('remove', 'remove_github_actions') + with self.command_group('functionapp deployment github-actions') as g: + g.custom_command('add', 'add_functionapp_github_actions') + g.custom_command('remove', 'remove_functoinapp_github_actions') + with self.command_group('webapp auth') as g: g.custom_show_command('show', 'get_auth_settings') g.custom_command('update', 'update_auth_settings') 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 080b9c8818d..b6bead38e0c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -73,7 +73,8 @@ FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, FUNCTIONS_NO_V2_REGIONS, 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, - WINDOWS_OS_NAME) + WINDOWS_OS_NAME, LINUX_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, + WINDOWS_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH) from ._github_oauth import (get_github_access_token) from ._validators import validate_and_convert_to_int, validate_range_of_int_flag @@ -5257,6 +5258,262 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None return "Disconnected successfully." +def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches + branch='master', build_path=".", login_with_github=False, force=False): + runtime = _StackRuntimeHelper(cmd).remove_delimiters(runtime) # normalize "runtime:version" + if not token and not login_with_github: + raise_missing_token_suggestion() + elif not token: + scopes = ["admin:repo_hook", "repo", "workflow"] + token = get_github_access_token(cmd, scopes) + elif token and login_with_github: + logger.warning("Both token and --login-with-github flag are provided. Will use provided token") + + # Verify resource group, app + # site_availability = get_site_availability(cmd, name) + # if site_availability.name_available or (not site_availability.name_available and + # site_availability.reason == 'Invalid'): + # raise ResourceNotFoundError( + # "The Resource 'Microsoft.Web/sites/%s' under resource group '%s' " + # "was not found." % (name, resource_group)) + # app_details = get_app_details(cmd, name) + # if app_details is None: + # raise ResourceNotFoundError( + # "Unable to retrieve details of the existing app %s. Please check that the app is a part of " + # "the current subscription" % name) + # current_rg = app_details.resource_group + # if resource_group is not None and (resource_group.lower() != current_rg.lower()): + # raise ResourceNotFoundError("The webapp %s exists in ResourceGroup %s and does not match the " + # "value entered %s. Please re-run command with the correct " + # "parameters." % (name, current_rg, resource_group)) + + app = show_app(cmd, resource_group, name, slot) + # parsed_plan_id = parse_resource_id(app_details.server_farm_id) + client = web_client_factory(cmd.cli_ctx) + # plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name']) + is_linux = app.reserved + + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException, UnknownObjectException + + if repo.strip()[-1] == '/': + repo = repo.strip()[:-1] + + g = Github(token) + github_repo = None + try: + github_repo = g.get_repo(repo) + try: + github_repo.get_branch(branch=branch) + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIError(error_msg) + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException: + raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " + "the --token argument. Run 'az webapp deployment github-actions add --help' " + "for more information.") + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} repo".format(repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIError(error_msg) + + # Verify runtime + app_runtime_info = _get_app_runtime_info( + cmd=cmd, resource_group=resource_group, name=name, slot=slot, is_linux=is_linux) + + app_runtime_string = None + if(app_runtime_info and app_runtime_info['display_name']): + app_runtime_string = app_runtime_info['display_name'] + + github_actions_version = None + if (app_runtime_info and app_runtime_info['github_actions_version']): + github_actions_version = app_runtime_info['github_actions_version'] + + if runtime and app_runtime_string: + if app_runtime_string.lower() != runtime.lower(): + logger.warning('The app runtime: {app_runtime_string} does not match the runtime specified: ' + '{runtime}. Using the specified runtime {runtime}.') + app_runtime_string = runtime + elif runtime: + app_runtime_string = runtime + + if not app_runtime_string: + raise ValidationError('Could not detect runtime. Please specify using the --runtime flag.') + + print(app_runtime_string) + + # if not _runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, is_linux=is_linux): + # raise ValidationError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) + + # Get workflow template + logger.warning('Getting workflow template using runtime: %s', app_runtime_string) + workflow_template = _get_functionapp_workflow_template(github=g, runtime_string=app_runtime_string, + is_linux=is_linux) + + + print(workflow_template.decoded_content.decode()) + + # Fill workflow template + # guid = str(uuid.uuid4()).replace('-', '') + # publish_profile_name = "AzureAppService_PublishProfile_{}".format(guid) + # logger.warning( + # 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', + # name, branch, github_actions_version, slot if slot else 'production') + completed_workflow_file = _fill_functionapp_workflow_template(content=workflow_template.decoded_content.decode(), + name=name, build_path=build_path, version=github_actions_version) + completed_workflow_file = completed_workflow_file.encode() + + + + + + # Check if workflow exists in repo, otherwise push + if slot: + file_name = "{}_{}({}).yml".format(branch.replace('/', '-'), name.lower(), slot) + else: + file_name = "{}_{}.yml".format(branch.replace('/', '-'), name.lower()) + dir_path = "{}/{}".format('.github', 'workflows') + file_path = "{}/{}".format(dir_path, file_name) + try: + existing_workflow_file = github_repo.get_contents(path=file_path, ref=branch) + existing_publish_profile_name = _get_publish_profile_from_workflow_file( + workflow_file=str(existing_workflow_file.decoded_content)) + if existing_publish_profile_name: + completed_workflow_file = completed_workflow_file.decode() + completed_workflow_file = completed_workflow_file.replace( + publish_profile_name, existing_publish_profile_name) + completed_workflow_file = completed_workflow_file.encode() + publish_profile_name = existing_publish_profile_name + logger.warning("Existing workflow file found") + if force: + logger.warning("Replacing the existing workflow file") + github_repo.update_file(path=file_path, message="Update workflow using Azure CLI", + content=completed_workflow_file, sha=existing_workflow_file.sha, branch=branch) + else: + option = prompt_y_n('Replace existing workflow file?') + if option: + logger.warning("Replacing the existing workflow file") + github_repo.update_file(path=file_path, message="Update workflow using Azure CLI", + content=completed_workflow_file, sha=existing_workflow_file.sha, + branch=branch) + else: + logger.warning("Use the existing workflow file") + if existing_publish_profile_name: + publish_profile_name = existing_publish_profile_name + except UnknownObjectException: + logger.warning("Creating new workflow file: %s", file_path) + github_repo.create_file(path=file_path, message="Create workflow using Azure CLI", + content=completed_workflow_file, branch=branch) + + # Add publish profile to GitHub + logger.warning('Adding publish profile to GitHub') + _add_publish_profile_to_github(cmd=cmd, resource_group=resource_group, name=name, repo=repo, + token=token, github_actions_secret_name=publish_profile_name, + slot=slot) + + # Set site source control properties + _update_site_source_control_properties_for_gh_action( + cmd=cmd, resource_group=resource_group, name=name, token=token, repo=repo, branch=branch, slot=slot) + + github_actions_url = "https://github.com/{}/actions".format(repo) + return github_actions_url + + +def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=None, slot=None, # pylint: disable=too-many-statements + branch='master', login_with_github=False): + if not token and not login_with_github: + raise_missing_token_suggestion() + elif not token: + scopes = ["admin:repo_hook", "repo", "workflow"] + token = get_github_access_token(cmd, scopes) + elif token and login_with_github: + logger.warning("Both token and --login-with-github flag are provided. Will use provided token") + + # Verify resource group, app + site_availability = get_site_availability(cmd, name) + if site_availability.name_available or (not site_availability.name_available and + site_availability.reason == 'Invalid'): + raise ResourceNotFoundError("The Resource 'Microsoft.Web/sites/%s' under resource group '%s' was not found." % + (name, resource_group)) + app_details = get_app_details(cmd, name) + if app_details is None: + raise ResourceNotFoundError("Unable to retrieve details of the existing app %s. " + "Please check that the app is a part of the current subscription" % name) + current_rg = app_details.resource_group + if resource_group is not None and (resource_group.lower() != current_rg.lower()): + raise ValidationError("The webapp %s exists in ResourceGroup %s and does not match " + "the value entered %s. Please re-run command with the correct " + "parameters." % (name, current_rg, resource_group)) + + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException, UnknownObjectException + + if repo.strip()[-1] == '/': + repo = repo.strip()[:-1] + + g = Github(token) + github_repo = None + try: + github_repo = g.get_repo(repo) + try: + github_repo.get_branch(branch=branch) + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIError(error_msg) + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException: + raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " + "the --token argument. Run 'az webapp deployment github-actions add --help' " + "for more information.") + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} repo".format(repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIError(error_msg) + + # Check if workflow exists in repo and remove + file_name = "{}_{}({}).yml".format( + branch.replace('/', '-'), name.lower(), slot) if slot else "{}_{}.yml".format( + branch.replace('/', '-'), name.lower()) + dir_path = "{}/{}".format('.github', 'workflows') + file_path = "{}/{}".format(dir_path, file_name) + existing_publish_profile_name = None + try: + existing_workflow_file = github_repo.get_contents(path=file_path, ref=branch) + existing_publish_profile_name = _get_publish_profile_from_workflow_file( + workflow_file=str(existing_workflow_file.decoded_content)) + logger.warning("Removing the existing workflow file") + github_repo.delete_file(path=file_path, message="Removing workflow file, disconnecting github actions", + sha=existing_workflow_file.sha, branch=branch) + except UnknownObjectException as e: + error_msg = "Error when removing workflow file." + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIError(error_msg) + + # Remove publish profile from GitHub + if existing_publish_profile_name: + logger.warning('Removing publish profile from GitHub') + _remove_publish_profile_from_github(cmd=cmd, resource_group=resource_group, name=name, repo=repo, token=token, + github_actions_secret_name=existing_publish_profile_name, slot=slot) + + # Remove site source control properties + delete_source_control(cmd=cmd, + resource_group_name=resource_group, + name=name, + slot=slot) + + return "Disconnected successfully." + + def _get_publish_profile_from_workflow_file(workflow_file): import re publish_profile = None @@ -5324,6 +5581,28 @@ def _get_workflow_template(github, runtime_string, is_linux): return file_contents +def _get_functionapp_workflow_template(github, runtime_string, is_linux): + from github import GithubException + + file_contents = None + template_repo_path = 'Azure/actions-workflow-samples' + template_path_map = (LINUX_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH if is_linux else + WINDOWS_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH) + template_file_path = _get_functionapp_template_file_path(runtime_string=runtime_string, + template_path_map=template_path_map) + + try: + template_repo = github.get_repo(template_repo_path) + file_contents = template_repo.get_contents(template_file_path) + except GithubException as e: + error_msg = "Encountered GitHub error when retrieving workflow template" + if e.data and e.data['message']: + error_msg += ": {}".format(e.data['message']) + raise CLIError(error_msg) + return file_contents + + + def _fill_workflow_template(content, name, branch, slot, publish_profile, version): if not slot: slot = 'production' @@ -5339,6 +5618,17 @@ def _fill_workflow_template(content, name, branch, slot, publish_profile, versio content = content.replace('${python-version}', version) return content +def _fill_functionapp_workflow_template(content, name, build_path, version): + content = content.replace("AZURE_FUNCTIONAPP_NAME: your-app-name", f"AZURE_FUNCTIONAPP_NAME: '{name}'") + content = content.replace("POM_FUNCTIONAPP_NAME: your-app-name", f"POM_FUNCTIONAPP_NAME: '{name}'") + content = content.replace("AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'", f"AZURE_FUNCTIONAPP_PACKAGE_PATH: '{build_path}'") + content = content.replace("POM_XML_DIRECTORY: '.'", f"POM_XML_DIRECTORY: '{build_path}'") + content = content.replace("DOTNET_VERSION: '2.2.402'", f"DOTNET_VERSION: '{version}'") + content = content.replace("JAVA_VERSION: '1.8.x'", f"JAVA_VERSION: '{version}'") + content = content.replace("NODE_VERSION: '10.x'", f"NODE_VERSION: '{version}'") + content = content.replace("PYTHON_VERSION: '3.7'", f"NODE_VERSION: '{version}'") + return content + def _get_template_file_path(runtime_string, is_linux): if not runtime_string: @@ -5366,6 +5656,21 @@ def _get_template_file_path(runtime_string, is_linux): return template_file_path +def _get_functionapp_template_file_path(runtime_string, template_path_map): + if not runtime_string: + raise ResourceNotFoundError('Unable to retrieve workflow template') + + runtime_string = runtime_string.lower() + runtime_stack = runtime_string.split('|')[0] + template_file_path = None + + template_file_path = template_path_map.get(runtime_stack) + + if not template_file_path: + raise ResourceNotFoundError('Unable to retrieve workflow template.') + return template_file_path + + def _add_publish_profile_to_github(cmd, resource_group, name, repo, token, github_actions_secret_name, slot=None): # Get publish profile with secrets import requests From 58aa2c7190a320abdd48d8cecc35d449033e4087 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 21 Jul 2022 15:32:39 -0700 Subject: [PATCH 02/15] Initial commit. --- .../command_modules/appservice/_constants.py | 1 + .../cli/command_modules/appservice/custom.py | 207 ++++++++++++++---- 2 files changed, 161 insertions(+), 47 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py index 247d36084a7..e9a9078b89f 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -53,6 +53,7 @@ def __init__(self): self.SUPPORTED_EXTENSION_VERSIONS = 'supportedFunctionsExtensionVersions' self.USE_32_BIT_WORKER_PROC = 'use32BitWorkerProcess' self.FUNCTIONS_WORKER_RUNTIME = 'FUNCTIONS_WORKER_RUNTIME' + self.GIT_HUB_ACTION_SETTINGS = 'git_hub_action_settings' GENERATE_RANDOM_APP_NAMES = os.path.abspath(os.path.join(os.path.abspath(__file__), 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 b6bead38e0c..a6d295d2011 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3194,7 +3194,7 @@ class _FunctionAppStackRuntimeHelper(_AbstractStackRuntimeHelper): # pylint: disable=too-few-public-methods,too-many-instance-attributes class Runtime: def __init__(self, name=None, version=None, is_preview=False, supported_func_versions=None, linux=False, - app_settings_dict=None, site_config_dict=None, app_insights=False, default=False): + app_settings_dict=None, site_config_dict=None, app_insights=False, default=False, github_actions_properties=None): self.name = name self.version = version self.is_preview = is_preview @@ -3204,6 +3204,7 @@ def __init__(self, name=None, version=None, is_preview=False, supported_func_ver self.site_config_dict = dict() if not site_config_dict else site_config_dict self.app_insights = app_insights self.default = default + self.github_actions_properties = github_actions_properties self.display_name = "{}|{}".format(name, version) if version else name @@ -3234,7 +3235,10 @@ def resolve(self, runtime, version=None, functions_version=None, linux=False): # help convert previously acceptable versions into correct ones if match not found old_to_new_version = { "11": "11.0", - "8": "8.0" + "8": "8.0", + "7": "7.0", + "6.0": "6", + "1.8": "8.0" } new_version = old_to_new_version.get(version) matched_runtime_version = next((r for r in runtimes if r.version == new_version), None) @@ -3305,6 +3309,7 @@ def _parse_minor_version(self, runtime_settings, major_version_name, minor_versi self.KEYS.APPLICATION_INSIGHTS: runtime_settings.app_insights_settings.is_supported, self.KEYS.SITE_CONFIG_DICT: runtime_settings.site_config_properties_dictionary, self.KEYS.IS_DEFAULT: bool(runtime_settings.is_default), + self.KEYS.GIT_HUB_ACTION_SETTINGS: runtime_settings.git_hub_action_settings } runtime_name = (runtime_settings.app_settings_dictionary.get(self.KEYS.FUNCTIONS_WORKER_RUNTIME) or @@ -3323,6 +3328,7 @@ def _create_runtime_from_properties(self, runtime_name, version_name, version_pr app_settings_dict=version_properties[self.KEYS.APP_SETTINGS_DICT], app_insights=version_properties[self.KEYS.APPLICATION_INSIGHTS], default=version_properties[self.KEYS.IS_DEFAULT], + github_actions_properties=version_properties[self.KEYS.GIT_HUB_ACTION_SETTINGS] ) def _parse_raw_stacks(self, stacks): @@ -5258,9 +5264,8 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None return "Disconnected successfully." -def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches +def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, runtime_version=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches branch='master', build_path=".", login_with_github=False, force=False): - runtime = _StackRuntimeHelper(cmd).remove_delimiters(runtime) # normalize "runtime:version" if not token and not login_with_github: raise_missing_token_suggestion() elif not token: @@ -5270,27 +5275,25 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None logger.warning("Both token and --login-with-github flag are provided. Will use provided token") # Verify resource group, app - # site_availability = get_site_availability(cmd, name) - # if site_availability.name_available or (not site_availability.name_available and - # site_availability.reason == 'Invalid'): - # raise ResourceNotFoundError( - # "The Resource 'Microsoft.Web/sites/%s' under resource group '%s' " - # "was not found." % (name, resource_group)) - # app_details = get_app_details(cmd, name) - # if app_details is None: - # raise ResourceNotFoundError( - # "Unable to retrieve details of the existing app %s. Please check that the app is a part of " - # "the current subscription" % name) - # current_rg = app_details.resource_group - # if resource_group is not None and (resource_group.lower() != current_rg.lower()): - # raise ResourceNotFoundError("The webapp %s exists in ResourceGroup %s and does not match the " - # "value entered %s. Please re-run command with the correct " - # "parameters." % (name, current_rg, resource_group)) + site_availability = get_site_availability(cmd, name) + if site_availability.name_available or (not site_availability.name_available and + site_availability.reason == 'Invalid'): + raise ResourceNotFoundError( + "The Resource 'Microsoft.Web/sites/%s' under resource group '%s' " + "was not found." % (name, resource_group)) + app_details = get_app_details(cmd, name) + if app_details is None: + raise ResourceNotFoundError( + "Unable to retrieve details of the existing app %s. Please check that the app is a part of " + "the current subscription" % name) + current_rg = app_details.resource_group + if resource_group is not None and (resource_group.lower() != current_rg.lower()): + raise ResourceNotFoundError("The webapp %s exists in ResourceGroup %s and does not match the " + "value entered %s. Please re-run command with the correct " + "parameters." % (name, current_rg, resource_group)) app = show_app(cmd, resource_group, name, slot) - # parsed_plan_id = parse_resource_id(app_details.server_farm_id) client = web_client_factory(cmd.cli_ctx) - # plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name']) is_linux = app.reserved # Verify github repo @@ -5320,10 +5323,10 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None error_msg = "Encountered GitHub error when accessing {} repo".format(repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise CLIError(error_msg) # Verify runtime - app_runtime_info = _get_app_runtime_info( + app_runtime_info = _get_functionapp_runtime_info( cmd=cmd, resource_group=resource_group, name=name, slot=slot, is_linux=is_linux) app_runtime_string = None @@ -5336,41 +5339,49 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None if runtime and app_runtime_string: if app_runtime_string.lower() != runtime.lower(): - logger.warning('The app runtime: {app_runtime_string} does not match the runtime specified: ' - '{runtime}. Using the specified runtime {runtime}.') + logger.warning(f'The app runtime: {app_runtime_string} does not match the runtime specified: ' + f'{runtime}. Using the specified runtime {runtime}.') app_runtime_string = runtime elif runtime: app_runtime_string = runtime + if runtime_version and github_actions_version: + if github_actions_version.lower() != runtime_version.lower(): + logger.warning(f'The app runtime version: {github_actions_version} does not match the runtime version specified: ' + f'{runtime_version}. Using the specified runtime {runtime_version}.') + github_actions_version = runtime_version + elif runtime_version: + github_actions_version = runtime_version + + if not app_runtime_string and not github_actions_version: + raise ValidationError('Could not detect runtime or runtime version. Please specify using the --runtime and --runtime-version flags.') + if not app_runtime_string: raise ValidationError('Could not detect runtime. Please specify using the --runtime flag.') - print(app_runtime_string) + if not github_actions_version: + raise ValidationError('Could not detect runtime version. Please specify using the --runtime-version flag.') - # if not _runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, is_linux=is_linux): - # raise ValidationError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) + if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, runtime_version=github_actions_version, functionapp_version=app_runtime_info['functionapp_version'], is_linux=is_linux): + raise ValidationError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) # Get workflow template logger.warning('Getting workflow template using runtime: %s', app_runtime_string) workflow_template = _get_functionapp_workflow_template(github=g, runtime_string=app_runtime_string, is_linux=is_linux) - - print(workflow_template.decoded_content.decode()) - # Fill workflow template - # guid = str(uuid.uuid4()).replace('-', '') - # publish_profile_name = "AzureAppService_PublishProfile_{}".format(guid) - # logger.warning( - # 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', - # name, branch, github_actions_version, slot if slot else 'production') + guid = str(uuid.uuid4()).replace('-', '') + publish_profile_name = "AZURE_FUNCTIONAPP_PUBLISH_PROFILE_{}".format(guid) + logger.warning( + 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', + name, branch, github_actions_version, slot if slot else 'production') completed_workflow_file = _fill_functionapp_workflow_template(content=workflow_template.decoded_content.decode(), - name=name, build_path=build_path, version=github_actions_version) + name=name, build_path=build_path, version=github_actions_version, + publish_profile=publish_profile_name) completed_workflow_file = completed_workflow_file.encode() - - - + 0/0 # Check if workflow exists in repo, otherwise push if slot: @@ -5602,7 +5613,6 @@ def _get_functionapp_workflow_template(github, runtime_string, is_linux): return file_contents - def _fill_workflow_template(content, name, branch, slot, publish_profile, version): if not slot: slot = 'production' @@ -5618,15 +5628,19 @@ def _fill_workflow_template(content, name, branch, slot, publish_profile, versio content = content.replace('${python-version}', version) return content -def _fill_functionapp_workflow_template(content, name, build_path, version): + +def _fill_functionapp_workflow_template(content, name, build_path, version, publish_profile): + content = content.replace("AZURE_FUNCTIONAPP_PUBLISH_PROFILE", f"{publish_profile}") content = content.replace("AZURE_FUNCTIONAPP_NAME: your-app-name", f"AZURE_FUNCTIONAPP_NAME: '{name}'") content = content.replace("POM_FUNCTIONAPP_NAME: your-app-name", f"POM_FUNCTIONAPP_NAME: '{name}'") content = content.replace("AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'", f"AZURE_FUNCTIONAPP_PACKAGE_PATH: '{build_path}'") content = content.replace("POM_XML_DIRECTORY: '.'", f"POM_XML_DIRECTORY: '{build_path}'") - content = content.replace("DOTNET_VERSION: '2.2.402'", f"DOTNET_VERSION: '{version}'") - content = content.replace("JAVA_VERSION: '1.8.x'", f"JAVA_VERSION: '{version}'") - content = content.replace("NODE_VERSION: '10.x'", f"NODE_VERSION: '{version}'") - content = content.replace("PYTHON_VERSION: '3.7'", f"NODE_VERSION: '{version}'") + content = content.replace("runs-on: ubuntu-18.04", "") + if version: + content = content.replace("DOTNET_VERSION: '2.2.402'", f"DOTNET_VERSION: '{version}.x'") + content = content.replace("JAVA_VERSION: '1.8.x'", f"JAVA_VERSION: '{version}'") + content = content.replace("NODE_VERSION: '10.x'", f"NODE_VERSION: '{version}.x'") + content = content.replace("PYTHON_VERSION: '3.7'", f"PYTHON_VERSION: '{version}'") return content @@ -5729,6 +5743,19 @@ def _runtime_supports_github_actions(cmd, runtime_string, is_linux): return False +def _functionapp_runtime_supports_github_actions(cmd, runtime_string, runtime_version, functionapp_version, is_linux): + import re + print(runtime_version) + runtime_version = re.sub(r"[^\d\.]", "", runtime_version).rstrip('.') + helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) + matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) + if not matched_runtime: + return False + if matched_runtime.github_actions_properties: + return True + return False + + def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): app_settings = None app_runtime = None @@ -5769,6 +5796,59 @@ def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): return _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux) +def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): + app_settings = None + app_runtime = None + functionapp_version = None + + app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + for app_setting in app_settings: + if 'name' in app_setting and app_setting['name'] == 'FUNCTIONS_EXTENSION_VERSION': + functionapp_version = app_setting["value"] + break + + if is_linux: + app_metadata = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + app_runtime = getattr(app_metadata, 'linux_fx_version', None) + return _get_functionapp_runtime_info_helper(cmd, app_runtime, None, functionapp_version, is_linux) + + if not app_runtime: + app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + for app_setting in app_settings: + if 'name' in app_setting and app_setting['name'] == 'FUNCTIONS_WORKER_RUNTIME': + app_runtime = app_setting["value"] + break + + if app_runtime and app_runtime.lower() == 'node': + app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + for app_setting in app_settings: + if 'name' in app_setting and app_setting['name'] == 'WEBSITE_NODE_DEFAULT_VERSION': + app_runtime_version = app_setting['value'] if 'value' in app_setting else None + if app_runtime_version: + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + elif app_runtime and app_runtime.lower() == 'python': + app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + app_runtime_version = getattr(app_settings, 'python_version', '') + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + elif app_runtime and app_runtime.lower() == 'dotnet': + app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + app_runtime_version = getattr(app_settings, 'net_framework_version', '') + # app_runtime_version = app_runtime_version.replace('.0', '') + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + elif app_runtime and app_runtime.lower() == 'java': + app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + app_runtime_version = java_version=getattr(app_settings, 'java_version', '').lower() + # if app_runtime_version == '1.8': + # app_runtime_version = '8.0' + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + elif app_runtime and app_runtime.lower() == 'powershell': + app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + app_runtime_version = java_version=getattr(app_settings, 'power_shell_version', '').lower() + # if '.' not in app_runtime_version: + # app_runtime_version += '.0' + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + + def _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux): helper = _StackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) if not is_linux: @@ -5791,6 +5871,39 @@ def _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux return None +def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux): + import re + + if is_linux: + app_runtime_version = app_runtime.split('|')[1] + app_runtime = app_runtime.split('|')[0].lower() + + # Normalize versions + functionapp_version = re.sub(r"[^\d\.]", "", functionapp_version) + app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) + + # Bug where dotnet 3.1 is set to 4.0 on portal (not tested on cli) + if not is_linux and app_runtime == 'dotnet': + if app_runtime_version == '4': + return { + "display_name": app_runtime, + "github_actions_version": None, + "functionapp_version": functionapp_version + } + helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) + matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux) + gh_props = None if not matched_runtime else matched_runtime.github_actions_properties + if gh_props: + if gh_props.supported_version: + return { + "display_name": app_runtime, + "github_actions_version": gh_props.supported_version, + "functionapp_version": functionapp_version + + } + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments on os %s." % (app_runtime, app_runtime_version, "linux" if is_linux else "windows")) + + def _encrypt_github_actions_secret(public_key, secret_value): # Encrypt a Unicode string using the public key from base64 import b64encode From 6ef79693656a43d6b1a2e74d9921f0353632ee67 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 21 Jul 2022 15:55:52 -0700 Subject: [PATCH 03/15] Fixed minor bugs. --- .../cli/command_modules/appservice/custom.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 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 a6d295d2011..bfecede7cbd 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5362,8 +5362,11 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None if not github_actions_version: raise ValidationError('Could not detect runtime version. Please specify using the --runtime-version flag.') - if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, runtime_version=github_actions_version, functionapp_version=app_runtime_info['functionapp_version'], is_linux=is_linux): - raise ValidationError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) + if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, runtime_version=github_actions_version, + functionapp_version=app_runtime_info['functionapp_version'], is_linux=is_linux): + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " + "on os %s for functionapp version %s." % (app_runtime_string, github_actions_version, + "linux" if is_linux else "windows"), app_runtime_info['functionapp_version']) # Get workflow template logger.warning('Getting workflow template using runtime: %s', app_runtime_string) @@ -5381,8 +5384,6 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None publish_profile=publish_profile_name) completed_workflow_file = completed_workflow_file.encode() - 0/0 - # Check if workflow exists in repo, otherwise push if slot: file_name = "{}_{}({}).yml".format(branch.replace('/', '-'), name.lower(), slot) @@ -5635,11 +5636,11 @@ def _fill_functionapp_workflow_template(content, name, build_path, version, publ content = content.replace("POM_FUNCTIONAPP_NAME: your-app-name", f"POM_FUNCTIONAPP_NAME: '{name}'") content = content.replace("AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'", f"AZURE_FUNCTIONAPP_PACKAGE_PATH: '{build_path}'") content = content.replace("POM_XML_DIRECTORY: '.'", f"POM_XML_DIRECTORY: '{build_path}'") - content = content.replace("runs-on: ubuntu-18.04", "") + content = content.replace("runs-on: ubuntu-18.04", "") # repair linux python yaml if version: - content = content.replace("DOTNET_VERSION: '2.2.402'", f"DOTNET_VERSION: '{version}.x'") + content = content.replace("DOTNET_VERSION: '2.2.402'", f"DOTNET_VERSION: '{version}'") content = content.replace("JAVA_VERSION: '1.8.x'", f"JAVA_VERSION: '{version}'") - content = content.replace("NODE_VERSION: '10.x'", f"NODE_VERSION: '{version}.x'") + content = content.replace("NODE_VERSION: '10.x'", f"NODE_VERSION: '{version}'") content = content.replace("PYTHON_VERSION: '3.7'", f"PYTHON_VERSION: '{version}'") return content @@ -5745,7 +5746,6 @@ def _runtime_supports_github_actions(cmd, runtime_string, is_linux): def _functionapp_runtime_supports_github_actions(cmd, runtime_string, runtime_version, functionapp_version, is_linux): import re - print(runtime_version) runtime_version = re.sub(r"[^\d\.]", "", runtime_version).rstrip('.') helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) @@ -5833,19 +5833,14 @@ def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): elif app_runtime and app_runtime.lower() == 'dotnet': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) app_runtime_version = getattr(app_settings, 'net_framework_version', '') - # app_runtime_version = app_runtime_version.replace('.0', '') return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) elif app_runtime and app_runtime.lower() == 'java': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) app_runtime_version = java_version=getattr(app_settings, 'java_version', '').lower() - # if app_runtime_version == '1.8': - # app_runtime_version = '8.0' return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) elif app_runtime and app_runtime.lower() == 'powershell': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) app_runtime_version = java_version=getattr(app_settings, 'power_shell_version', '').lower() - # if '.' not in app_runtime_version: - # app_runtime_version += '.0' return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) @@ -5882,14 +5877,15 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version = re.sub(r"[^\d\.]", "", functionapp_version) app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) - # Bug where dotnet 3.1 is set to 4.0 on portal (not tested on cli) + # Bug where dotnet 3.1 is set to 4.0 on portal and cli if not is_linux and app_runtime == 'dotnet': - if app_runtime_version == '4': + if app_runtime_version == '4.0': return { "display_name": app_runtime, "github_actions_version": None, "functionapp_version": functionapp_version } + helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux) gh_props = None if not matched_runtime else matched_runtime.github_actions_properties From 7abb7626320833827d3ff16f189d03572c410dfc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 22 Jul 2022 15:23:34 -0700 Subject: [PATCH 04/15] Fixed bug. --- .../azure/cli/command_modules/appservice/commands.py | 2 +- src/azure-cli/azure/cli/command_modules/appservice/custom.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 3716a5166f3..708fccff2b7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -261,7 +261,7 @@ def load_command_table(self, _): with self.command_group('functionapp deployment github-actions') as g: g.custom_command('add', 'add_functionapp_github_actions') - g.custom_command('remove', 'remove_functoinapp_github_actions') + g.custom_command('remove', 'remove_functionapp_github_actions') with self.command_group('webapp auth') as g: g.custom_show_command('show', 'get_auth_settings') 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 bfecede7cbd..9ed55ea9ff5 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5878,8 +5878,8 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) # Bug where dotnet 3.1 is set to 4.0 on portal and cli - if not is_linux and app_runtime == 'dotnet': - if app_runtime_version == '4.0': + if app_runtime == 'dotnet': + if app_runtime_version != '6.0': return { "display_name": app_runtime, "github_actions_version": None, From dbef922974f8cee1c423ff2011aca64bf20e36b9 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 25 Jul 2022 12:21:52 -0700 Subject: [PATCH 05/15] Fixed minor syntax issue. --- .../azure/cli/command_modules/appservice/custom.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 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 9ed55ea9ff5..40ae6f5bb5a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5812,13 +5812,12 @@ def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): app_runtime = getattr(app_metadata, 'linux_fx_version', None) return _get_functionapp_runtime_info_helper(cmd, app_runtime, None, functionapp_version, is_linux) - if not app_runtime: - app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) - for app_setting in app_settings: - if 'name' in app_setting and app_setting['name'] == 'FUNCTIONS_WORKER_RUNTIME': - app_runtime = app_setting["value"] - break - + app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) + for app_setting in app_settings: + if 'name' in app_setting and app_setting['name'] == 'FUNCTIONS_WORKER_RUNTIME': + app_runtime = app_setting["value"] + break + if app_runtime and app_runtime.lower() == 'node': app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) for app_setting in app_settings: From 9ea02d2c62ef3e972dfdcaa3abe98e433b0dd40f Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 25 Jul 2022 14:11:33 -0700 Subject: [PATCH 06/15] Fixed style issues. --- .../cli/command_modules/appservice/custom.py | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 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 40ae6f5bb5a..52a90d53dce 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3194,7 +3194,8 @@ class _FunctionAppStackRuntimeHelper(_AbstractStackRuntimeHelper): # pylint: disable=too-few-public-methods,too-many-instance-attributes class Runtime: def __init__(self, name=None, version=None, is_preview=False, supported_func_versions=None, linux=False, - app_settings_dict=None, site_config_dict=None, app_insights=False, default=False, github_actions_properties=None): + app_settings_dict=None, site_config_dict=None, app_insights=False, default=False, + github_actions_properties=None): self.name = name self.version = version self.is_preview = is_preview @@ -5264,8 +5265,8 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None return "Disconnected successfully." -def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, runtime_version=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches - branch='master', build_path=".", login_with_github=False, force=False): +def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, runtime_version=None, token=None, # pylint: disable=too-many-statements,too-many-branches + slot=None, branch='master', build_path=".", login_with_github=False, force=False): if not token and not login_with_github: raise_missing_token_suggestion() elif not token: @@ -5293,7 +5294,6 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None "parameters." % (name, current_rg, resource_group)) app = show_app(cmd, resource_group, name, slot) - client = web_client_factory(cmd.cli_ctx) is_linux = app.reserved # Verify github repo @@ -5323,7 +5323,7 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None error_msg = "Encountered GitHub error when accessing {} repo".format(repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise CLIError(error_msg) # Verify runtime app_runtime_info = _get_functionapp_runtime_info( @@ -5339,22 +5339,24 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None if runtime and app_runtime_string: if app_runtime_string.lower() != runtime.lower(): - logger.warning(f'The app runtime: {app_runtime_string} does not match the runtime specified: ' - f'{runtime}. Using the specified runtime {runtime}.') + logger.warning('The app runtime: %s does not match the runtime specified: ' + '%s. Using the specified runtime %s.', app_runtime_string, runtime, runtime) app_runtime_string = runtime elif runtime: app_runtime_string = runtime if runtime_version and github_actions_version: if github_actions_version.lower() != runtime_version.lower(): - logger.warning(f'The app runtime version: {github_actions_version} does not match the runtime version specified: ' - f'{runtime_version}. Using the specified runtime {runtime_version}.') + logger.warning('The app runtime version: %s does not match the runtime version specified: ' + '%s. Using the specified runtime %s.', github_actions_version, runtime_version, + runtime_version) github_actions_version = runtime_version elif runtime_version: github_actions_version = runtime_version if not app_runtime_string and not github_actions_version: - raise ValidationError('Could not detect runtime or runtime version. Please specify using the --runtime and --runtime-version flags.') + raise ValidationError('Could not detect runtime or runtime version. Please specify' + 'using the --runtime and --runtime-version flags.') if not app_runtime_string: raise ValidationError('Could not detect runtime. Please specify using the --runtime flag.') @@ -5362,11 +5364,14 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None if not github_actions_version: raise ValidationError('Could not detect runtime version. Please specify using the --runtime-version flag.') - if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, runtime_version=github_actions_version, - functionapp_version=app_runtime_info['functionapp_version'], is_linux=is_linux): + if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, + runtime_version=github_actions_version, + functionapp_version=app_runtime_info['functionapp_version'], + is_linux=is_linux): raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " "on os %s for functionapp version %s." % (app_runtime_string, github_actions_version, - "linux" if is_linux else "windows"), app_runtime_info['functionapp_version']) + "linux" if is_linux else "windows", + app_runtime_info['functionapp_version'])) # Get workflow template logger.warning('Getting workflow template using runtime: %s', app_runtime_string) @@ -5380,7 +5385,8 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', name, branch, github_actions_version, slot if slot else 'production') completed_workflow_file = _fill_functionapp_workflow_template(content=workflow_template.decoded_content.decode(), - name=name, build_path=build_path, version=github_actions_version, + name=name, build_path=build_path, + version=github_actions_version, publish_profile=publish_profile_name) completed_workflow_file = completed_workflow_file.encode() @@ -5437,7 +5443,7 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=None, slot=None, # pylint: disable=too-many-statements - branch='master', login_with_github=False): + branch='master', login_with_github=False): if not token and not login_with_github: raise_missing_token_suggestion() elif not token: @@ -5817,30 +5823,35 @@ def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): if 'name' in app_setting and app_setting['name'] == 'FUNCTIONS_WORKER_RUNTIME': app_runtime = app_setting["value"] break - + if app_runtime and app_runtime.lower() == 'node': app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) for app_setting in app_settings: if 'name' in app_setting and app_setting['name'] == 'WEBSITE_NODE_DEFAULT_VERSION': app_runtime_version = app_setting['value'] if 'value' in app_setting else None if app_runtime_version: - return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, + functionapp_version, is_linux) elif app_runtime and app_runtime.lower() == 'python': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) app_runtime_version = getattr(app_settings, 'python_version', '') - return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, + is_linux) elif app_runtime and app_runtime.lower() == 'dotnet': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) app_runtime_version = getattr(app_settings, 'net_framework_version', '') - return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, + is_linux) elif app_runtime and app_runtime.lower() == 'java': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) - app_runtime_version = java_version=getattr(app_settings, 'java_version', '').lower() - return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + app_runtime_version = getattr(app_settings, 'java_version', '').lower() + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, + is_linux) elif app_runtime and app_runtime.lower() == 'powershell': app_settings = get_site_configs(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) - app_runtime_version = java_version=getattr(app_settings, 'power_shell_version', '').lower() - return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + app_runtime_version = getattr(app_settings, 'power_shell_version', '').lower() + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, + is_linux) def _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux): @@ -5896,7 +5907,8 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, "functionapp_version": functionapp_version } - raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments on os %s." % (app_runtime, app_runtime_version, "linux" if is_linux else "windows")) + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments on os %s." % + (app_runtime, app_runtime_version, "linux" if is_linux else "windows")) def _encrypt_github_actions_secret(public_key, secret_value): From 8b4fce77282eabeeb992ff82cd77298a9099c514 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 25 Jul 2022 14:32:35 -0700 Subject: [PATCH 07/15] Added help and params. --- .../cli/command_modules/appservice/_help.py | 29 +++++++++++++++++++ .../cli/command_modules/appservice/_params.py | 15 ++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index d4bf390a52c..9fa83dbeb05 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -605,6 +605,35 @@ az functionapp deployment user set --user-name MyUserName """ +helps['functionapp deployment github-actions'] = """ +type: group +short-summary: Configure GitHub Actions for a functionapp +""" + +helps['functionapp deployment github-actions add'] = """ +type: command +short-summary: Add a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified functionapp. +examples: + - name: Add GitHub Actions to a specified repository, providing personal access token + text: > + az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken + - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token + text: > + az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github +""" + +helps['functionapp deployment github-actions remove'] = """ +type: command +short-summary: Remove and disconnect the GitHub Actions workflow file from the specified repository. +examples: + - name: Remove GitHub Actions from a specified repository, providing personal access token + text: > + az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken + - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token + text: > + az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github +""" + helps['functionapp function'] = """ type: group short-summary: Manage function app functions. 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 c116c3d0290..8d04d0d876f 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -824,6 +824,21 @@ def load_arguments(self, _): help="swap types. use 'preview' to apply target slot's settings on the source slot first; use 'swap' to complete it; use 'reset' to reset the swap", arg_type=get_enum_type(['swap', 'preview', 'reset'])) + with self.argument_context('functionapp deployment github-actions')as c: + c.argument('name', arg_type=functionapp_name_arg_type) + c.argument('resource_group', arg_type=resource_group_name_type, options_list=['--resource-group', '-g']) + c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: /') + c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') + c.argument('slot', options_list=['--slot', '-s'], help='The name of the slot. Default to the production slot if not specified.') + c.argument('branch', options_list=['--branch', '-b'], help='The branch to which the workflow file will be added. Defaults to "master" if not specified.') + c.argument('login_with_github', help='Interactively log in with GitHub to retrieve the Personal Access Token', action='store_true') + + with self.argument_context('functionapp deployment github-actions add')as c: + c.argument('runtime', options_list=['--runtime', '-r'], help='The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions.') + c.argument('runtime_version', options_list=['--runtime-version', '-v'], help='The version of the functions runtime stack. The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions.') + c.argument('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.', action='store_true') + c.argument('build_path', help='Path to the build requirements. Ex: package path, POM XML directory.') + with self.argument_context('functionapp keys', id_part=None) as c: c.argument('resource_group_name', arg_type=resource_group_name_type,) c.argument('name', arg_type=functionapp_name_arg_type, From 5b28578575c3ea9fc0e05d7f04a96230e8827bba Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 25 Jul 2022 16:24:14 -0700 Subject: [PATCH 08/15] Made slight improvements in error-handling in response to feedback. --- .../cli/command_modules/appservice/custom.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 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 52a90d53dce..2ef75892011 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3220,7 +3220,7 @@ def __init__(self, cmd, linux=False, windows=False): self.KEYS = FUNCTIONS_STACKS_API_KEYS() super().__init__(cmd, linux=linux, windows=windows) - def resolve(self, runtime, version=None, functions_version=None, linux=False): + def resolve(self, runtime, version=None, functions_version=None, linux=False, disable_version_error=False): stacks = self.stacks runtimes = [r for r in stacks if r.linux == linux and runtime == r.name] os = LINUX_OS_NAME if linux else WINDOWS_OS_NAME @@ -3245,6 +3245,8 @@ def resolve(self, runtime, version=None, functions_version=None, linux=False): matched_runtime_version = next((r for r in runtimes if r.version == new_version), None) if not matched_runtime_version: versions = [r.version for r in runtimes] + if disable_version_error: + return None raise ValidationError("Invalid version: {0} for runtime {1} and os {2}. Supported versions for runtime " "{1} and os {2} are: {3}. " "Run 'az functionapp list-runtimes' for more details on supported runtimes. " @@ -5364,14 +5366,15 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None if not github_actions_version: raise ValidationError('Could not detect runtime version. Please specify using the --runtime-version flag.') - if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, - runtime_version=github_actions_version, - functionapp_version=app_runtime_info['functionapp_version'], - is_linux=is_linux): - raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " - "on os %s for functionapp version %s." % (app_runtime_string, github_actions_version, - "linux" if is_linux else "windows", - app_runtime_info['functionapp_version'])) + # We only need to check if runtime_version is passed + if runtime_version: + if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, + runtime_version=github_actions_version, + functionapp_version=app_runtime_info['functionapp_version'], + is_linux=is_linux): + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " + "on os %s." % (app_runtime_string, github_actions_version, + "linux" if is_linux else "windows")) # Get workflow template logger.warning('Getting workflow template using runtime: %s', app_runtime_string) @@ -5382,8 +5385,8 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None guid = str(uuid.uuid4()).replace('-', '') publish_profile_name = "AZURE_FUNCTIONAPP_PUBLISH_PROFILE_{}".format(guid) logger.warning( - 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s', - name, branch, github_actions_version, slot if slot else 'production') + 'Filling workflow template with name: %s, branch: %s, version: %s, slot: %s, build_path: %s', + name, branch, github_actions_version, slot if slot else 'production', build_path) completed_workflow_file = _fill_functionapp_workflow_template(content=workflow_template.decoded_content.decode(), name=name, build_path=build_path, version=github_actions_version, @@ -5758,7 +5761,8 @@ def _functionapp_runtime_supports_github_actions(cmd, runtime_string, runtime_ve if not matched_runtime: return False if matched_runtime.github_actions_properties: - return True + if matched_runtime.github_actions_properties.supported_version: + return True return False @@ -5887,17 +5891,9 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version = re.sub(r"[^\d\.]", "", functionapp_version) app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) - # Bug where dotnet 3.1 is set to 4.0 on portal and cli - if app_runtime == 'dotnet': - if app_runtime_version != '6.0': - return { - "display_name": app_runtime, - "github_actions_version": None, - "functionapp_version": functionapp_version - } - helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) - matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux) + matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux, + disable_version_error=True) gh_props = None if not matched_runtime else matched_runtime.github_actions_properties if gh_props: if gh_props.supported_version: @@ -5905,10 +5901,12 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, "display_name": app_runtime, "github_actions_version": gh_props.supported_version, "functionapp_version": functionapp_version - } - raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments on os %s." % - (app_runtime, app_runtime_version, "linux" if is_linux else "windows")) + return { + "display_name": app_runtime, + "github_actions_version": None, + "functionapp_version": functionapp_version + } def _encrypt_github_actions_secret(public_key, secret_value): From d7d786e71aa3f43f4019cfdaa1339101badd6ecc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 11:49:19 -0700 Subject: [PATCH 09/15] Resolved PR comments: added token caching, added error handling for custom runtime, added repo url to repo name conversion, removed all references of CLIError. --- .../appservice/_github_oauth.py | 65 +++++++++++++++++-- .../cli/command_modules/appservice/_help.py | 4 +- .../cli/command_modules/appservice/_params.py | 5 +- .../cli/command_modules/appservice/custom.py | 38 +++++------ .../cli/command_modules/appservice/utils.py | 20 ++++++ 5 files changed, 100 insertions(+), 32 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py b/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py index 5f18c69e349..c43f257f3fa 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py @@ -2,11 +2,18 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=consider-using-f-string + +import os +import sys +from datetime import datetime -from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) from knack.log import get_logger +from azure.cli.core.util import open_page_in_browser +from azure.cli.core.auth.persistence import SecretStore, build_persistence +from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) -from ._constants import (GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_SCOPES) +from .utils import repo_url_to_name logger = get_logger(__name__) @@ -17,7 +24,56 @@ ''' -def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument +GITHUB_OAUTH_CLIENT_ID = "8d8e1f6000648c575489" +GITHUB_OAUTH_SCOPES = [ + "admin:repo_hook", + "repo", + "workflow" +] + + +def _get_github_token_secret_store(cmd): + location = os.path.join(cmd.cli_ctx.config.config_dir, "github_token_cache") + # TODO use core CLI util to take care of this once it's merged and released + encrypt = sys.platform.startswith('win32') # encryption not supported on non-windows platforms + file_persistence = build_persistence(location, encrypt) + return SecretStore(file_persistence) + + +def cache_github_token(cmd, token, repo): + repo = repo_url_to_name(repo) + secret_store = _get_github_token_secret_store(cmd) + cache = secret_store.load() + + for entry in cache: + if isinstance(entry, dict) and entry.get("value") == token: + if repo not in entry.get("repos", []): + entry["repos"] = [*entry.get("repos", []), repo] + entry["last_modified_timestamp"] = datetime.utcnow().timestamp() + break + else: + cache_entry = {"last_modified_timestamp": datetime.utcnow().timestamp(), "value": token, "repos": [repo]} + cache = [cache_entry, *cache] + + secret_store.save(cache) + + +def load_github_token_from_cache(cmd, repo): + repo = repo_url_to_name(repo) + secret_store = _get_github_token_secret_store(cmd) + cache = secret_store.load() + + if isinstance(cache, list): + for entry in cache: + if isinstance(entry, dict) and repo in entry.get("repos", []): + return entry.get("value") + + return None + + +def get_github_access_token(cmd, scope_list=None, token=None): # pylint: disable=unused-argument + if token: + return token if scope_list: for scope in scope_list: if scope not in GITHUB_OAUTH_SCOPES: @@ -45,6 +101,7 @@ def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-arg expires_in_seconds = int(parsed_response['expires_in'][0]) logger.warning('Please navigate to %s and enter the user code %s to activate and ' 'retrieve your github personal access token', verification_uri, user_code) + open_page_in_browser("https://github.com/login/device") timeout = time.time() + expires_in_seconds logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60)) @@ -76,6 +133,6 @@ def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-arg return parsed_confirmation_response['access_token'][0] except Exception as e: raise CLIInternalError( - 'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e)) + 'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e)) from e raise UnclassifiedUserFault('Activation did not happen in time. Please try again') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index 9fa83dbeb05..6b38d8af765 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -619,7 +619,7 @@ az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token text: > - az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github + az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp """ helps['functionapp deployment github-actions remove'] = """ @@ -631,7 +631,7 @@ az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token text: > - az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github + az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp """ helps['functionapp function'] = """ 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 8d04d0d876f..2596580620a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -826,12 +826,11 @@ def load_arguments(self, _): with self.argument_context('functionapp deployment github-actions')as c: c.argument('name', arg_type=functionapp_name_arg_type) - c.argument('resource_group', arg_type=resource_group_name_type, options_list=['--resource-group', '-g']) - c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: /') + c.argument('resource_group', arg_type=resource_group_name_type) + c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com// or /') c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') c.argument('slot', options_list=['--slot', '-s'], help='The name of the slot. Default to the production slot if not specified.') c.argument('branch', options_list=['--branch', '-b'], help='The branch to which the workflow file will be added. Defaults to "master" if not specified.') - c.argument('login_with_github', help='Interactively log in with GitHub to retrieve the Personal Access Token', action='store_true') with self.argument_context('functionapp deployment github-actions add')as c: c.argument('runtime', options_list=['--runtime', '-r'], help='The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions.') 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 2ef75892011..a9e894188a7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -47,7 +47,7 @@ from azure.cli.core.azclierror import (InvalidArgumentValueError, MutuallyExclusiveArgumentError, ResourceNotFoundError, RequiredArgumentMissingError, ValidationError, CLIInternalError, UnclassifiedUserFault, AzureResponseError, AzureInternalError, - ArgumentUsageError) + ArgumentUsageError, FileOperationError) from .tunnel import TunnelServer @@ -64,7 +64,7 @@ _get_location_from_webapp, _normalize_location, get_pool_manager, use_additional_properties, get_app_service_plan_from_webapp, - get_resource_if_exists) + get_resource_if_exists, repo_url_to_name, get_token) from ._create_util import (zip_contents_from_dir, get_runtime_version_details, create_resource_group, get_app_details, check_resource_group_exists, set_location, get_site_availability, get_profile_username, get_plan_to_use, get_lang_from_content, get_rg_to_use, get_sku_to_use, @@ -75,7 +75,7 @@ DOTNET_RUNTIME_NAME, NETCORE_RUNTIME_NAME, ASPDOTNET_RUNTIME_NAME, LINUX_OS_NAME, WINDOWS_OS_NAME, LINUX_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, WINDOWS_FUNCTIONAPP_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH) -from ._github_oauth import (get_github_access_token) +from ._github_oauth import (get_github_access_token, cache_github_token) from ._validators import validate_and_convert_to_int, validate_range_of_int_flag logger = get_logger(__name__) @@ -5268,14 +5268,9 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, runtime_version=None, token=None, # pylint: disable=too-many-statements,too-many-branches - slot=None, branch='master', build_path=".", login_with_github=False, force=False): - if not token and not login_with_github: - raise_missing_token_suggestion() - elif not token: - scopes = ["admin:repo_hook", "repo", "workflow"] - token = get_github_access_token(cmd, scopes) - elif token and login_with_github: - logger.warning("Both token and --login-with-github flag are provided. Will use provided token") + slot=None, branch='master', build_path=".", force=False): + repo = repo_url_to_name(repo) + token = get_token(cmd, repo, token) # Verify resource group, app site_availability = get_site_availability(cmd, name) @@ -5325,7 +5320,7 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None error_msg = "Encountered GitHub error when accessing {} repo".format(repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise ValidationError(error_msg) # Verify runtime app_runtime_info = _get_functionapp_runtime_info( @@ -5441,20 +5436,15 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None _update_site_source_control_properties_for_gh_action( cmd=cmd, resource_group=resource_group, name=name, token=token, repo=repo, branch=branch, slot=slot) + cache_github_token(cmd, token, repo) github_actions_url = "https://github.com/{}/actions".format(repo) return github_actions_url def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=None, slot=None, # pylint: disable=too-many-statements - branch='master', login_with_github=False): - if not token and not login_with_github: - raise_missing_token_suggestion() - elif not token: - scopes = ["admin:repo_hook", "repo", "workflow"] - token = get_github_access_token(cmd, scopes) - elif token and login_with_github: - logger.warning("Both token and --login-with-github flag are provided. Will use provided token") - + branch='master'): + repo = repo_url_to_name(repo) + token = get_token(cmd, repo, token) # Verify resource group, app site_availability = get_site_availability(cmd, name) if site_availability.name_available or (not site_availability.name_available and @@ -5498,7 +5488,7 @@ def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=Non error_msg = "Encountered GitHub error when accessing {} repo".format(repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise ValidationError(error_msg) # Check if workflow exists in repo and remove file_name = "{}_{}({}).yml".format( @@ -5518,7 +5508,7 @@ def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=Non error_msg = "Error when removing workflow file." if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise FileOperationError(error_msg) # Remove publish profile from GitHub if existing_publish_profile_name: @@ -5884,6 +5874,8 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, import re if is_linux: + if len(app_runtime.split('|')) < 2: + raise ValidationError(f"Runtime {app_runtime} is not supported.") app_runtime_version = app_runtime.split('|')[1] app_runtime = app_runtime.split('|')[0].lower() diff --git a/src/azure-cli/azure/cli/command_modules/appservice/utils.py b/src/azure-cli/azure/cli/command_modules/appservice/utils.py index 62a1280ce2f..01515acce40 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -227,3 +227,23 @@ def use_additional_properties(resource): resource.enable_additional_properties_sending() existing_properties = resource.serialize().get("properties") resource.additional_properties["properties"] = {} if existing_properties is None else existing_properties + + +def repo_url_to_name(repo_url): + repo = None + repo = [s for s in repo_url.split('/') if s] + if len(repo) >= 2: + repo = '/'.join(repo[-2:]) + return repo + + +def get_token(cmd, repo, token): + from ._github_oauth import load_github_token_from_cache, get_github_access_token + if not repo: + return None + if token: + return token + token = load_github_token_from_cache(cmd, repo) + if not token: + token = get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) + return token From 713d633d9ea233b233ca0a8538534b75825c5310 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 14:04:08 -0700 Subject: [PATCH 10/15] Added validator to check if app is functionapp. Resolved pr param comment. --- .../command_modules/appservice/_constants.py | 2 ++ .../cli/command_modules/appservice/_params.py | 2 +- .../command_modules/appservice/_validators.py | 32 ++++++++++++++++++- .../command_modules/appservice/commands.py | 7 ++-- .../cli/command_modules/appservice/utils.py | 19 +++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py index e9a9078b89f..7d584953637 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -30,6 +30,8 @@ "repo", "workflow" ] +LOGICAPP_KIND = "workflowapp" +FUNCTIONAPP_KIND = "functionapp" class FUNCTIONS_STACKS_API_KEYS(): 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 8f546e07c31..79d69efdb20 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -830,7 +830,7 @@ def load_arguments(self, _): c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com// or /') c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') c.argument('slot', options_list=['--slot', '-s'], help='The name of the slot. Default to the production slot if not specified.') - c.argument('branch', options_list=['--branch', '-b'], help='The branch to which the workflow file will be added. Defaults to "master" if not specified.') + c.argument('branch', options_list=['--branch', '-b'], help='The branch to which the workflow file will be added.') with self.argument_context('functionapp deployment github-actions add')as c: c.argument('runtime', options_list=['--runtime', '-r'], help='The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions.') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py index 6eb269fb8bd..53e228b6c80 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py @@ -18,7 +18,7 @@ from ._appservice_utils import _generic_site_operation from ._client_factory import web_client_factory from .utils import (_normalize_sku, get_sku_tier, _normalize_location, get_resource_name_and_group, - get_resource_if_exists) + get_resource_if_exists, is_functionapp, is_logicapp, is_webapp) logger = get_logger(__name__) @@ -390,3 +390,33 @@ def validate_webapp_up(cmd, namespace): ase = client.app_service_environments.get(resource_group_name=ase_rg, name=ase_name) _validate_ase_is_v3(ase) _validate_ase_not_ilb(ase) + + +def _get_app_name(namespace): + if hasattr(namespace, "name"): + return namespace.name + if hasattr(namespace, "webapp"): + return namespace.webapp + return None + + +def validate_app_is_webapp(cmd, namespace): + client = web_client_factory(cmd.cli_ctx) + name = _get_app_name(namespace) + rg = namespace.resource_group + app = get_resource_if_exists(client.web_apps, name=name, resource_group_name=rg) + if is_functionapp(app): + raise ValidationError(f"App '{name}' in group '{rg}' is a function app.") + if is_logicapp(app): + raise ValidationError(f"App '{name}' in group '{rg}' is a logic app.") + + +def validate_app_is_functionapp(cmd, namespace): + client = web_client_factory(cmd.cli_ctx) + name = _get_app_name(namespace) + rg = namespace.resource_group + app = get_resource_if_exists(client.web_apps, name=name, resource_group_name=rg) + if is_logicapp(app): + raise ValidationError(f"App '{name}' in group '{rg}' is a logic app.") + if is_webapp(app): + raise ValidationError(f"App '{name}' in group '{rg}' is a web app.") 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 708fccff2b7..da1c1963490 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -10,7 +10,8 @@ from ._client_factory import cf_web_client, cf_plans, cf_webapps from ._validators import (validate_onedeploy_params, validate_staticsite_link_function, validate_staticsite_sku, validate_vnet_integration, validate_asp_create, validate_functionapp_asp_create, - validate_webapp_up, validate_app_exists, validate_add_vnet) + validate_webapp_up, validate_app_exists, validate_add_vnet, validate_app_is_functionapp, + validate_app_is_webapp) def output_slots_in_table(slots): @@ -255,11 +256,11 @@ def load_command_table(self, _): g.custom_command('config', 'enable_cd') g.custom_command('show-cd-url', 'show_container_cd_url') - with self.command_group('webapp deployment github-actions') as g: + with self.command_group('webapp deployment github-actions', validator=validate_app_is_webapp) as g: g.custom_command('add', 'add_github_actions') g.custom_command('remove', 'remove_github_actions') - with self.command_group('functionapp deployment github-actions') as g: + with self.command_group('functionapp deployment github-actions', validator=validate_app_is_functionapp) as g: g.custom_command('add', 'add_functionapp_github_actions') g.custom_command('remove', 'remove_functionapp_github_actions') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/utils.py b/src/azure-cli/azure/cli/command_modules/appservice/utils.py index 01515acce40..c4f2510ead0 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -19,6 +19,7 @@ from msrestazure.tools import parse_resource_id, is_valid_resource_id, resource_id from ._client_factory import web_client_factory +from ._constants import LOGICAPP_KIND, FUNCTIONAPP_KIND logger = get_logger(__name__) @@ -247,3 +248,21 @@ def get_token(cmd, repo, token): if not token: token = get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) return token + + +def is_logicapp(app): + if app is None or app.kind is None: + return False + return LOGICAPP_KIND in app.kind + + +def is_functionapp(app): + if app is None or app.kind is None: + return False + return not is_logicapp(app) and FUNCTIONAPP_KIND in app.kind + + +def is_webapp(app): + if app is None or app.kind is None: + return False + return not is_logicapp(app) and not is_functionapp(app) and "app" in app.kind From 2bd2986481d2d8c98224860afa1f997ada882c19 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 15:32:27 -0700 Subject: [PATCH 11/15] Added back --login-with-github to refresh token in cache. --- .../cli/command_modules/appservice/_help.py | 4 ++-- .../cli/command_modules/appservice/_params.py | 3 ++- .../cli/command_modules/appservice/custom.py | 20 ++++++++++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index 6b38d8af765..9fa83dbeb05 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -619,7 +619,7 @@ az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token text: > - az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp + az functionapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github """ helps['functionapp deployment github-actions remove'] = """ @@ -631,7 +631,7 @@ az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --token MyPersonalAccessToken - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token text: > - az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp + az functionapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyFunctionapp --login-with-github """ helps['functionapp function'] = """ 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 79d69efdb20..0c7289d650d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -828,9 +828,10 @@ def load_arguments(self, _): c.argument('name', arg_type=functionapp_name_arg_type) c.argument('resource_group', arg_type=resource_group_name_type) c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com// or /') - c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') + c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line', arg_group="Github") c.argument('slot', options_list=['--slot', '-s'], help='The name of the slot. Default to the production slot if not specified.') c.argument('branch', options_list=['--branch', '-b'], help='The branch to which the workflow file will be added.') + c.argument('login_with_github', help="Interactively log in with Github to retrieve the Personal Access Token", arg_group="Github") with self.argument_context('functionapp deployment github-actions add')as c: c.argument('runtime', options_list=['--runtime', '-r'], help='The functions runtime stack. Use "az functionapp list-runtimes" to check supported runtimes and versions.') 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 b05f3e2156a..fc808f34182 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5270,7 +5270,9 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None, runtime_version=None, token=None, # pylint: disable=too-many-statements,too-many-branches - slot=None, branch='master', build_path=".", force=False): + slot=None, branch='master', build_path=".", login_with_github=False, force=False): + if login_with_github: + token = get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) repo = repo_url_to_name(repo) token = get_token(cmd, repo, token) @@ -5312,11 +5314,11 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise ValidationError(error_msg) logger.warning('Verified GitHub repo and branch') except BadCredentialsException: raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " - "the --token argument. Run 'az webapp deployment github-actions add --help' " + "the --token argument. Run 'az functionapp deployment github-actions add --help' " "for more information.") except GithubException as e: error_msg = "Encountered GitHub error when accessing {} repo".format(repo) @@ -5444,7 +5446,9 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=None, slot=None, # pylint: disable=too-many-statements - branch='master'): + branch='master', login_with_github=False): + if login_with_github: + token = get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) repo = repo_url_to_name(repo) token = get_token(cmd, repo, token) # Verify resource group, app @@ -5459,7 +5463,7 @@ def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=Non "Please check that the app is a part of the current subscription" % name) current_rg = app_details.resource_group if resource_group is not None and (resource_group.lower() != current_rg.lower()): - raise ValidationError("The webapp %s exists in ResourceGroup %s and does not match " + raise ValidationError("The functionapp %s exists in ResourceGroup %s and does not match " "the value entered %s. Please re-run command with the correct " "parameters." % (name, current_rg, resource_group)) @@ -5480,11 +5484,11 @@ def remove_functionapp_github_actions(cmd, resource_group, name, repo, token=Non error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) - raise CLIError(error_msg) + raise ValidationError(error_msg) logger.warning('Verified GitHub repo and branch') except BadCredentialsException: raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " - "the --token argument. Run 'az webapp deployment github-actions add --help' " + "the --token argument. Run 'az functionapp deployment github-actions remove --help' " "for more information.") except GithubException as e: error_msg = "Encountered GitHub error when accessing {} repo".format(repo) @@ -5635,6 +5639,8 @@ def _fill_functionapp_workflow_template(content, name, build_path, version, publ content = content.replace("AZURE_FUNCTIONAPP_PUBLISH_PROFILE", f"{publish_profile}") content = content.replace("AZURE_FUNCTIONAPP_NAME: your-app-name", f"AZURE_FUNCTIONAPP_NAME: '{name}'") content = content.replace("POM_FUNCTIONAPP_NAME: your-app-name", f"POM_FUNCTIONAPP_NAME: '{name}'") + if "AZURE_FUNCTIONAPP_PACKAGE_PATH" not in content and "POM_XML_DIRECTORY" not in content: + logger.warning("Runtime does not support --build-path, ignoring value.") content = content.replace("AZURE_FUNCTIONAPP_PACKAGE_PATH: '.'", f"AZURE_FUNCTIONAPP_PACKAGE_PATH: '{build_path}'") content = content.replace("POM_XML_DIRECTORY: '.'", f"POM_XML_DIRECTORY: '{build_path}'") content = content.replace("runs-on: ubuntu-18.04", "") # repair linux python yaml From 45a4c28649b4ad4c03ebc832d61702632619b6fc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 16:42:36 -0700 Subject: [PATCH 12/15] Change supported user passed --runtime-version to supported API gh-action version. --- .../cli/command_modules/appservice/custom.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 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 fc808f34182..47791a3f11e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5367,12 +5367,14 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None # We only need to check if runtime_version is passed if runtime_version: - if not _functionapp_runtime_supports_github_actions(cmd=cmd, runtime_string=app_runtime_string, - runtime_version=github_actions_version, - functionapp_version=app_runtime_info['functionapp_version'], - is_linux=is_linux): + 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, + functionapp_version=functionapp_version, + is_linux=is_linux) + if not github_actions_version: raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " - "on os %s." % (app_runtime_string, github_actions_version, + "on os %s." % (app_runtime_string, runtime_version, "linux" if is_linux else "windows")) # Get workflow template @@ -5751,17 +5753,17 @@ def _runtime_supports_github_actions(cmd, runtime_string, is_linux): return False -def _functionapp_runtime_supports_github_actions(cmd, runtime_string, runtime_version, functionapp_version, is_linux): +def _get_functionapp_runtime_version(cmd, runtime_string, runtime_version, functionapp_version, is_linux): import re runtime_version = re.sub(r"[^\d\.]", "", runtime_version).rstrip('.') helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) if not matched_runtime: - return False + return None if matched_runtime.github_actions_properties: if matched_runtime.github_actions_properties.supported_version: - return True - return False + return matched_runtime.github_actions_properties.supported_version + return None def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): From a0539e6ce6d2b8ad4a549afdc1beb1b364b28d7c Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 17:01:49 -0700 Subject: [PATCH 13/15] Added better error handling. --- .../cli/command_modules/appservice/custom.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 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 47791a3f11e..23e6564c545 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5894,8 +5894,16 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) - matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux, - disable_version_error=True) + try: + matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux) + except ValidationError as e: + if app_runtime == "dotnet": # catch issue with dotnet 3.1/4 + return { + "display_name": app_runtime, + "github_actions_version": None, + "functionapp_version": functionapp_version + } + raise e gh_props = None if not matched_runtime else matched_runtime.github_actions_properties if gh_props: if gh_props.supported_version: @@ -5904,11 +5912,9 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, "github_actions_version": gh_props.supported_version, "functionapp_version": functionapp_version } - return { - "display_name": app_runtime, - "github_actions_version": None, - "functionapp_version": functionapp_version - } + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " + "on os %s." % (app_runtime, app_runtime_version, + "linux" if is_linux else "windows")) def _encrypt_github_actions_secret(public_key, secret_value): From cc4d493a9f3d3d663730410a8df9339f2542fe46 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 26 Jul 2022 18:13:11 -0700 Subject: [PATCH 14/15] Refactored code to run less duplicate code. --- .../cli/command_modules/appservice/custom.py | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 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 23e6564c545..2876807d5c0 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5326,56 +5326,45 @@ def add_functionapp_github_actions(cmd, resource_group, name, repo, runtime=None error_msg += " Error: {}".format(e.data['message']) raise ValidationError(error_msg) - # Verify runtime + # Get runtime info app_runtime_info = _get_functionapp_runtime_info( cmd=cmd, resource_group=resource_group, name=name, slot=slot, is_linux=is_linux) - app_runtime_string = None - if(app_runtime_info and app_runtime_info['display_name']): - app_runtime_string = app_runtime_info['display_name'] - - github_actions_version = None - if (app_runtime_info and app_runtime_info['github_actions_version']): - github_actions_version = app_runtime_info['github_actions_version'] + app_runtime_string = app_runtime_info['app_runtime'] + github_actions_version = app_runtime_info['app_runtime_version'] - if runtime and app_runtime_string: - if app_runtime_string.lower() != runtime.lower(): + if runtime: + if app_runtime_string and app_runtime_string.lower() != runtime.lower(): logger.warning('The app runtime: %s does not match the runtime specified: ' '%s. Using the specified runtime %s.', app_runtime_string, runtime, runtime) - app_runtime_string = runtime - elif runtime: app_runtime_string = runtime - if runtime_version and github_actions_version: - if github_actions_version.lower() != runtime_version.lower(): + if runtime_version: + if github_actions_version and github_actions_version.lower() != runtime_version.lower(): logger.warning('The app runtime version: %s does not match the runtime version specified: ' '%s. Using the specified runtime %s.', github_actions_version, runtime_version, runtime_version) - github_actions_version = runtime_version - elif runtime_version: github_actions_version = runtime_version if not app_runtime_string and not github_actions_version: raise ValidationError('Could not detect runtime or runtime version. Please specify' 'using the --runtime and --runtime-version flags.') - if not app_runtime_string: raise ValidationError('Could not detect runtime. Please specify using the --runtime flag.') - if not github_actions_version: raise ValidationError('Could not detect runtime version. Please specify using the --runtime-version flag.') - # We only need to check if runtime_version is passed - if runtime_version: - 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, - functionapp_version=functionapp_version, - is_linux=is_linux) - if not github_actions_version: - raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " - "on os %s." % (app_runtime_string, runtime_version, - "linux" if is_linux else "windows")) + # 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, + functionapp_version=functionapp_version, + is_linux=is_linux) + if not github_actions_version: + runtime_version = runtime_version if runtime_version else app_runtime_info['app_runtime_version'] + raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " + "on os %s." % (app_runtime_string, runtime_version, + "linux" if is_linux else "windows")) # Get workflow template logger.warning('Getting workflow template using runtime: %s', app_runtime_string) @@ -5756,8 +5745,18 @@ def _runtime_supports_github_actions(cmd, runtime_string, is_linux): def _get_functionapp_runtime_version(cmd, runtime_string, runtime_version, functionapp_version, is_linux): import re runtime_version = re.sub(r"[^\d\.]", "", runtime_version).rstrip('.') + matched_runtime = None helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) - matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) + try: + matched_runtime = helper.resolve(runtime_string, runtime_version, functionapp_version, is_linux) + 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.") + error_message = e.error_msg[0:index] + error_message += "Try passing --runtime-version with a supported version, or " + 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: @@ -5806,10 +5805,11 @@ def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): return _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux) -def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): +def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): # pylint: disable=too-many-return-statements app_settings = None app_runtime = None functionapp_version = None + app_runtime_version = None app_settings = get_app_settings(cmd=cmd, resource_group_name=resource_group, name=name, slot=slot) for app_setting in app_settings: @@ -5856,6 +5856,7 @@ def _get_functionapp_runtime_info(cmd, resource_group, name, slot, is_linux): app_runtime_version = getattr(app_settings, 'power_shell_version', '').lower() return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) + return _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, functionapp_version, is_linux) def _get_app_runtime_info_helper(cmd, app_runtime, app_runtime_version, is_linux): @@ -5890,31 +5891,16 @@ def _get_functionapp_runtime_info_helper(cmd, app_runtime, app_runtime_version, app_runtime = app_runtime.split('|')[0].lower() # Normalize versions + functionapp_version = functionapp_version if functionapp_version else "" + app_runtime_version = app_runtime_version if app_runtime_version else "" functionapp_version = re.sub(r"[^\d\.]", "", functionapp_version) app_runtime_version = re.sub(r"[^\d\.]", "", app_runtime_version) - helper = _FunctionAppStackRuntimeHelper(cmd, linux=(is_linux), windows=(not is_linux)) - try: - matched_runtime = helper.resolve(app_runtime, app_runtime_version, functionapp_version, is_linux) - except ValidationError as e: - if app_runtime == "dotnet": # catch issue with dotnet 3.1/4 - return { - "display_name": app_runtime, - "github_actions_version": None, - "functionapp_version": functionapp_version - } - raise e - gh_props = None if not matched_runtime else matched_runtime.github_actions_properties - if gh_props: - if gh_props.supported_version: - return { - "display_name": app_runtime, - "github_actions_version": gh_props.supported_version, - "functionapp_version": functionapp_version - } - raise ValidationError("Runtime %s version %s is not supported for GitHub Actions deployments " - "on os %s." % (app_runtime, app_runtime_version, - "linux" if is_linux else "windows")) + return { + "app_runtime": app_runtime, + "app_runtime_version": app_runtime_version, + "functionapp_version": functionapp_version + } def _encrypt_github_actions_secret(public_key, secret_value): From 1348b20b55a332345b9c5f7cafb5954141500205 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 27 Jul 2022 10:58:12 -0700 Subject: [PATCH 15/15] Fixed powershell linux edge case. --- .../azure/cli/command_modules/appservice/custom.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 2876807d5c0..143e77ecfee 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5760,8 +5760,12 @@ def _get_functionapp_runtime_version(cmd, runtime_string, runtime_version, funct if not matched_runtime: return None if matched_runtime.github_actions_properties: - if matched_runtime.github_actions_properties.supported_version: - return matched_runtime.github_actions_properties.supported_version + gh_props = matched_runtime.github_actions_properties + if gh_props.is_supported: + if not is_linux and runtime_string.lower() == "powershell": + return runtime_version + # when stacks api is fixed, return supported_version if not null else runtime_verson + return gh_props.supported_version return None