From e01aa77277134d4764c43d0941d46af5ec74deea Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 21 May 2021 17:14:42 -0700 Subject: [PATCH 1/8] Github actions --- .../command_modules/appservice/_constants.py | 16 + .../cli/command_modules/appservice/_help.py | 10 + .../cli/command_modules/appservice/_params.py | 14 + .../command_modules/appservice/commands.py | 4 + .../cli/command_modules/appservice/custom.py | 448 +++++++++++++++++- .../resources/WebappRuntimeStacks.json | 152 +++++- src/azure-cli/setup.py | 1 + 7 files changed, 641 insertions(+), 4 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 ed1923cec35..8902cb07381 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,19 @@ def __init__(self): '../resources/GenerateRandomAppNames.json')) PUBLIC_CLOUD = "AzureCloud" + +LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { + 'node': 'AppService/linux/nodejs-webapp-on-azure.yml', + 'python': 'AppService/linux/python-webapp-on-azure.yml', + 'dotnetcore': 'AppService/linux/aspnet-core-webapp-on-azure.yml', + 'java': 'AppService/linux/java-jar-webapp-on-azure.yml', + 'tomcat': 'AppService/linux/java-war-webapp-on-azure.yml' +} + +WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { + 'node': 'AppService/windows/nodejs-webapp-on-azure.yml', + 'python': 'AppService/windows/python-webapp-on-azure.yml', + 'dotnetcore': 'AppService/windows/aspnet-core-webapp-on-azure.yml', + 'java': 'AppService/windows/java-jar-webapp-on-azure.yml', + 'tomcat': 'AppService/windows/java-war-webapp-on-azure.yml' +} 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 c27c01309b8..7f60b5c62c2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1715,6 +1715,16 @@ az webapp deployment user show """ +helps['webapp deployment github-actions add'] = """ + type: command + short-summary: Adds a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified webapp. +""" + +helps['webapp deployment github-actions remove'] = """ + type: command + short-summary: Removes and disconnects the GitHub Actions workflow file from the specified repository. +""" + helps['webapp hybrid-connection'] = """ type: group short-summary: methods that list, add and remove hybrid-connections from webapps 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 c2329dd86e5..2b04179d5b8 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -410,6 +410,20 @@ def load_arguments(self, _): c.argument('action', 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('webapp deployment github-actions')as c: + c.argument('name', arg_type=webapp_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('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.') + + with self.argument_context('webapp deployment github-actions add')as c: + c.argument('runtime', options_list=['--runtime', '-r'], help='Canonicalized web runtime in the format of Framework|Version, e.g. "PHP|5.6". Use "az webapp list-runtimes" for available list.') + c.argument('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.') + with self.argument_context('webapp log config') as c: c.argument('application_logging', help='configure application logging', arg_type=get_enum_type(['filesystem', 'azureblobstorage', 'off'])) 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 e7549eca506..9d5838fa6f6 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -247,6 +247,10 @@ 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: + g.custom_command('add', 'add_github_actions') + g.custom_command('remove', 'remove_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 f6a2b01707c..196bf7ae453 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -11,6 +11,7 @@ from urllib.parse import urlparse except ImportError: from urlparse import urlparse # pylint: disable=import-error +from base64 import b64encode from binascii import hexlify from os import urandom import datetime @@ -19,12 +20,13 @@ import sys import uuid from functools import reduce +from nacl import encoding, public from six.moves.urllib.request import urlopen # pylint: disable=import-error, ungrouped-imports import OpenSSL.crypto from fabric import Connection -from knack.prompting import prompt_pass, NoTTYException +from knack.prompting import prompt_pass, NoTTYException, prompt_y_n from knack.util import CLIError from knack.log import get_logger @@ -61,7 +63,9 @@ detect_os_form_src, get_current_stack_from_runtime, generate_default_app_name) from ._constants import (FUNCTIONS_STACKS_API_JSON_PATHS, FUNCTIONS_STACKS_API_KEYS, FUNCTIONS_LINUX_RUNTIME_VERSION_REGEX, FUNCTIONS_WINDOWS_RUNTIME_VERSION_REGEX, - NODE_EXACT_VERSION_DEFAULT, RUNTIME_STACKS, FUNCTIONS_NO_V2_REGIONS, PUBLIC_CLOUD) + NODE_EXACT_VERSION_DEFAULT, RUNTIME_STACKS, FUNCTIONS_NO_V2_REGIONS, PUBLIC_CLOUD, + LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH, WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH) +from ._github_oauth import (get_github_access_token) logger = get_logger(__name__) @@ -4459,3 +4463,443 @@ def delete_function_key(cmd, resource_group_name, name, key_name, function_name= if slot: return client.web_apps.delete_function_secret_slot(resource_group_name, name, function_name, key_name, slot) return client.web_apps.delete_function_secret(resource_group_name, name, function_name, key_name) + + +def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, branch='master', force=False): + if not token: + token = get_github_access_token(cmd, ['admin:repo_hook', 'repo', 'workflow']) + if not token: + raise CLIError("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.") + + # 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/{}' under resource group '{}' was not found.".format(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 {}. Please check that the app is a part of the current subscription".format(name)) + current_rg = app_details.resource_group + if resource_group is not None and (resource_group.lower() != current_rg.lower()): + raise ResourceNotFoundError("The webapp {} exists in ResourceGroup {} and does not match the value entered {}. Please " + "re-run command with the correct parameters.".format(name, current_rg, resource_group)) + 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 = plan_info.reserved + + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException, UnknownObjectException + import yaml + + 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 as e: + raise CLIError("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 = app_runtime_info['display_name'] if (app_runtime_info and app_runtime_info['display_name']) else None + github_actions_version = app_runtime_info['github_actions_version'] if (app_runtime_info and app_runtime_info['github_actions_version']) else None + + if runtime and app_runtime_string: + if app_runtime_string.lower() != runtime.lower(): + logger.warning('The app runtime: {} does not match the runtime specified: {}. Using the specified runtime {}.'.format(app_runtime_string, runtime, runtime)) + app_runtime_string = runtime + elif runtime: + app_runtime_string = runtime + + if not app_runtime_string: + raise CLIError('Could not detect runtime. Please specify using the --runtime flag.') + + if not _runtime_supports_github_actions(runtime_string=app_runtime_string, is_linux=is_linux): + raise CLIError("Runtime {} is not supported for GitHub Actions deployments.".format(app_runtime_string)) + + # Get workflow template + logger.warning('Getting workflow template using runtime: {}'.format(app_runtime_string)) + workflow_template = _get_workflow_template(github=g, runtime_string=app_runtime_string, is_linux=is_linux) + + # Fill workflow template + import uuid + guid = str(uuid.uuid4()).replace('-', '') + publish_profile_name = "AzureAppService_PublishProfile_{}".format(guid) + logger.warning('Fill workflow template with name: {}, branch: {}, version: {}, slot: {}'.format(name, branch, github_actions_version, slot if slot else 'production')) + completed_workflow_file = _fill_workflow_template(content=workflow_template.decoded_content.decode(), name=name, + branch=branch, slot=slot, publish_profile=publish_profile_name, version=github_actions_version) + completed_workflow_file = completed_workflow_file.encode() + + # Check if workflow exists in repo, otherwise push + 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) + 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 as e: + logger.warning("Creating new workflow file: {}".format(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('Add 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_github_actions(cmd, resource_group, name, repo, token=None, slot=None, branch='master'): + if not token: + token = get_github_access_token(cmd, ['admin:repo_hook', 'repo', 'workflow']) + if not token: + raise CLIError("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.") + + # 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 CLIError("The Resource 'Microsoft.Web/sites/{}' under resource group '{}' was not found.".format(name, resource_group)) + app_details = get_app_details(cmd, name) + if app_details is None: + raise CLIError("Unable to retrieve details of the existing app {}. Please check that the app is a part of the current subscription".format(name)) + current_rg = app_details.resource_group + if resource_group is not None and (resource_group.lower() != current_rg.lower()): + raise CLIError("The webapp {} exists in ResourceGroup {} and does not match the value entered {}. Please " + "re-run command with the correct parameters.".format(name, current_rg, resource_group)) + + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException, UnknownObjectException + import yaml + + 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 as e: + raise CLIError("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 + regex = re.search(r'publish-profile: \$\{\{ secrets\..*?\}\}', workflow_file) + if regex: + publish_profile = regex.group() + publish_profile = publish_profile.replace('publish-profile: ${{ secrets.', '') + publish_profile = publish_profile[:-2] + + if publish_profile: + return publish_profile.strip() + return None + + +def _update_site_source_control_properties_for_gh_action(cmd, resource_group, name, token, repo=None, branch="master", slot=None): + if repo: + repo_url = 'https://github.com/' + repo + else: + repo_url = None + + site_source_control = show_source_control(cmd=cmd, + resource_group_name=resource_group, + name=name, + slot=slot) + if site_source_control: + if not repo_url: + repo_url = site_source_control.repo_url + + delete_source_control(cmd=cmd, + resource_group_name=resource_group, + name=name, + slot=slot) + config_source_control(cmd=cmd, + resource_group_name=resource_group, + name=name, + repo_url=repo_url, + repository_type='github', + github_action=True, + branch=branch, + git_token=token, + slot=slot) + + +def _get_workflow_template(github, runtime_string, is_linux): + from github import GithubException + from github.GithubException import BadCredentialsException + + file_contents = None + template_repo_path = 'Azure/actions-workflow-templates' + template_file_path = _get_template_file_path(runtime_string=runtime_string, is_linux=is_linux) + + try: + template_repo = github.get_repo(template_repo_path) + file_contents = template_repo.get_contents(template_file_path) + except BadCredentialsException as e: + raise CLIError("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 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' + + content = content.replace('${web-app-name}', name) + content = content.replace('${branch}', branch) + content = content.replace('${slot-name}', slot) + content = content.replace('${azure-webapp-publish-profile-name}', publish_profile) + content = content.replace('${AZURE_WEBAPP_PUBLISH_PROFILE}', publish_profile) + content = content.replace('${dotnet-core-version}', version) + content = content.replace('${java-version}', version) + content = content.replace('${node-version}', version) + content = content.replace('${python-version}', version) + return content + + +def _get_template_file_path(runtime_string, is_linux): + if not runtime_string: + raise CLIError('Unable to retrieve workflow template') + + runtime_string = runtime_string.lower() + runtime_stack = runtime_string.split('|')[0] + template_file_path = None + + if is_linux: + template_file_path = LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH.get(runtime_stack, None) + else: + # Handle java naming + if runtime_stack == 'java': + java_container_split = runtime_string.split('|') + if java_container_split and len(java_container_split) >= 2: + if java_container_split[2] == 'tomcat': + runtime_stack = 'tomcat' + elif java_container_split[2] == 'java se': + runtime_stack = 'java' + template_file_path = WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH.get(runtime_stack, None) + + if not template_file_path: + raise CLIError('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 + + logger.warning("Fetching publish profile with secrets for the app '%s'", name) + publish_profile_bytes= _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_publishing_profile_xml_with_secrets', slot, {"format": "WebDeploy"}) + publish_profile = [x for x in publish_profile_bytes] + if publish_profile: + publish_profile = publish_profile[0].decode('ascii') + else: + raise CLIError('Unable to retrieve publish profile.') + + # Add publish profile with secrets as a GitHub Actions Secret in the repo + headers = {} + headers['Authorization'] = 'Token {}'.format(token) + headers['Content-Type'] = 'application/json;' + headers['Accept'] = 'application/json;' + + public_key_url = "https://api.github.com/repos/{}/actions/secrets/public-key".format(repo) + public_key = requests.get(public_key_url, headers=headers) + if not public_key.ok: + raise CLIError('Request to GitHub for public key failed.') + public_key = public_key.json() + + encrypted_github_actions_secret = _encrypt_github_actions_secret(public_key=public_key['key'], secret_value=str(publish_profile)) + payload = { + "encrypted_value": encrypted_github_actions_secret, + "key_id": public_key['key_id'] + } + + import json + store_secret_url = "https://api.github.com/repos/{}/actions/secrets/{}".format(repo, github_actions_secret_name) + stored_secret = requests.put(store_secret_url, data=json.dumps(payload), headers=headers) + if str(stored_secret.status_code)[0] != '2': + raise CLIError('Unable to add publish profile to GitHub. Request status code: {}'.format(stored_secret.status_code)) + + +def _remove_publish_profile_from_github(cmd, resource_group, name, repo, token, github_actions_secret_name, slot=None): + headers = {} + headers['Authorization'] = 'Token {}'.format(token) + + import requests + store_secret_url = "https://api.github.com/repos/{}/actions/secrets/{}".format(repo, github_actions_secret_name) + requests.delete(store_secret_url, headers=headers) + + +def _runtime_supports_github_actions(runtime_string, is_linux): + if is_linux: + stacks = get_file_json(RUNTIME_STACKS)['linux'] + else: + stacks = get_file_json(RUNTIME_STACKS)['windows'] + + supports = False + for stack in stacks: + if stack['displayName'].lower() == runtime_string.lower(): + if 'github_actions_properties' in stack and stack['github_actions_properties']: + supports = True + return supports + + +def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): # {'display_name': '', 'github_actions_version': ''} + app_settings = None + app_runtime = None + + 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_app_runtime_info_helper(app_runtime, "", is_linux) + else: + app_metadata = _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_metadata', slot) + app_metadata_properties = getattr(app_metadata, 'properties', {}) + if 'CURRENT_STACK' in app_metadata_properties: + app_runtime = app_metadata_properties['CURRENT_STACK'] + + 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_app_runtime_info_helper(app_runtime, app_runtime_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_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) + elif app_runtime and app_runtime.lower() == 'dotnetcore': + app_runtime_version = '3.1' + app_runtime_version = "" + return _get_app_runtime_info_helper(app_runtime, app_runtime_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}, {java_container}, {java_container_version}".format( + java_version=getattr(app_settings, 'java_version', '').lower(), + java_container=getattr(app_settings, 'java_container', '').lower(), + java_container_version=getattr(app_settings, 'java_container_version', '').lower() + ) + return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) + return None + + +def _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux): + if is_linux: + stacks = get_file_json(RUNTIME_STACKS)['linux'] + for stack in stacks: + if 'github_actions_properties' in stack and stack['github_actions_properties']: + if stack['displayName'].lower() == app_runtime.lower(): + return { + "display_name": stack['displayName'], + "github_actions_version": stack['github_actions_properties']['github_actions_version'] + } + else: + stacks = get_file_json(RUNTIME_STACKS)['windows'] + for stack in stacks: + if 'github_actions_properties' in stack and stack['github_actions_properties']: + if (stack['github_actions_properties']['app_runtime'].lower() == app_runtime.lower() and + stack['github_actions_properties']['app_runtime_version'].lower() == app_runtime_version.lower()): + return { + "display_name": stack['displayName'], + "github_actions_version": stack['github_actions_properties']['github_actions_version'] + } + return None + + +def _encrypt_github_actions_secret(public_key, secret_value): + # Encrypt a Unicode string using the public key + public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) + sealed_box = public.SealedBox(public_key) + encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) + return b64encode(encrypted).decode("utf-8") \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/appservice/resources/WebappRuntimeStacks.json b/src/azure-cli/azure/cli/command_modules/appservice/resources/WebappRuntimeStacks.json index 3411d0083d8..3801b6db10c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/resources/WebappRuntimeStacks.json +++ b/src/azure-cli/azure/cli/command_modules/appservice/resources/WebappRuntimeStacks.json @@ -14,11 +14,21 @@ }, { "displayName": "DOTNETCORE|2.1", - "configs": {} + "configs": {}, + "github_actions_properties": { + "app_runtime": "dotnetcore", + "app_runtime_version": "", + "github_actions_version": "2.1.804" + } }, { "displayName": "DOTNETCORE|3.1", - "configs": {} + "configs": {}, + "github_actions_properties": { + "app_runtime": "dotnetcore", + "app_runtime_version": "", + "github_actions_version": "3.1.102" + } }, { "displayName": "DOTNET|5.0", @@ -36,30 +46,55 @@ "displayName": "node|10.6", "configs": { "WEBSITE_NODE_DEFAULT_VERSION": "10.6.0" + }, + "github_actions_properties": { + "app_runtime": "node", + "app_runtime_version": "10.6.0", + "github_actions_version": "10.6.0" } }, { "displayName": "node|10.10", "configs": { "WEBSITE_NODE_DEFAULT_VERSION": "10.0.0" + }, + "github_actions_properties": { + "app_runtime": "node", + "app_runtime_version": "10.0.0", + "github_actions_version": "10.0.0" } }, { "displayName": "node|10.14", "configs": { "WEBSITE_NODE_DEFAULT_VERSION": "10.14.1" + }, + "github_actions_properties": { + "app_runtime": "node", + "app_runtime_version": "10.14.1", + "github_actions_version": "10.14.1" } }, { "displayName": "node|12-lts", "configs": { "WEBSITE_NODE_DEFAULT_VERSION": "12.13.0" + }, + "github_actions_properties": { + "app_runtime": "node", + "app_runtime_version": "12.13.0", + "github_actions_version": "12.13.0" } }, { "displayName": "node|14-lts", "configs": { "WEBSITE_NODE_DEFAULT_VERSION": "~14" + }, + "github_actions_properties": { + "app_runtime": "node", + "app_runtime_version": "~14", + "github_actions_version": "14" } }, { @@ -78,6 +113,11 @@ "displayName": "python|3.6", "configs": { "python_version": "3.4.0" + }, + "github_actions_properties": { + "app_runtime": "python", + "app_runtime_version": "3.4.0", + "github_actions_version": "3.6" } }, { @@ -86,6 +126,11 @@ "java_version": "1.8", "java_container": "TOMCAT", "java_container_version": "7.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "1.8, tomcat, 7.0", + "github_actions_version": "8" } }, { @@ -94,6 +139,11 @@ "java_version": "1.8", "java_container": "TOMCAT", "java_container_version": "8.5" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "1.8, tomcat, 8.5", + "github_actions_version": "8" } }, { @@ -102,6 +152,11 @@ "java_version": "1.8", "java_container": "TOMCAT", "java_container_version": "9.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "1.8, tomcat, 9.0", + "github_actions_version": "8" } }, { @@ -110,6 +165,11 @@ "java_version": "1.8", "java_container": "JAVA", "java_container_version": "SE" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "1.8, java, se", + "github_actions_version": "8" } }, { @@ -118,6 +178,11 @@ "java_version": "1.7", "java_container": "TOMCAT", "java_container_version": "7.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "7, tomcat, 7.0", + "github_actions_version": "8" } }, { @@ -126,6 +191,11 @@ "java_version": "1.7", "java_container": "TOMCAT", "java_container_version": "8.5" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "7, tomcat, 8.5", + "github_actions_version": "8" } }, { @@ -134,6 +204,11 @@ "java_version": "1.7", "java_container": "TOMCAT", "java_container_version": "9.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "7, tomcat, 9.0", + "github_actions_version": "8" } }, { @@ -142,6 +217,11 @@ "java_version": "1.7", "java_container": "JAVA", "java_container_version": "SE" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "7, java, se", + "github_actions_version": "8" } }, { @@ -150,6 +230,11 @@ "java_version": "11", "java_container": "TOMCAT", "java_container_version": "7.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "11, tomcat, 7.0", + "github_actions_version": "11" } }, { @@ -158,6 +243,11 @@ "java_version": "11", "java_container": "TOMCAT", "java_container_version": "8.5" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "11, tomcat, 8.5", + "github_actions_version": "11" } }, { @@ -166,6 +256,11 @@ "java_version": "11", "java_container": "TOMCAT", "java_container_version": "9.0" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "11, tomcat, 9.0", + "github_actions_version": "11" } }, { @@ -174,6 +269,11 @@ "java_version": "11", "java_container": "JAVA", "java_container_version": "SE" + }, + "github_actions_properties": { + "app_runtime": "java", + "app_runtime_version": "11, java, se", + "github_actions_version": "11" } } ], @@ -182,12 +282,18 @@ "displayName": "DOTNETCORE|2.1", "configs": { "linux_fx_version": "DOTNETCORE|2.1" + }, + "github_actions_properties": { + "github_actions_version": "2.1.804" } }, { "displayName": "DOTNETCORE|3.1", "configs": { "linux_fx_version": "DOTNETCORE|3.1" + }, + "github_actions_properties": { + "github_actions_version": "3.1.102" } }, { @@ -212,66 +318,99 @@ "displayName": "NODE|12-lts", "configs": { "linux_fx_version": "NODE|12-lts" + }, + "github_actions_properties": { + "github_actions_version": "12.x" } }, { "displayName": "NODE|10-lts", "configs": { "linux_fx_version": "NODE|10-lts" + }, + "github_actions_properties": { + "github_actions_version": "10.x" } }, { "displayName": "NODE|10.1", "configs": { "linux_fx_version": "NODE|10.1" + }, + "github_actions_properties": { + "github_actions_version": "10.1" } }, { "displayName": "NODE|10.6", "configs": { "linux_fx_version": "NODE|10.6" + }, + "github_actions_properties": { + "github_actions_version": "10.6" } }, { "displayName": "NODE|10.14", "configs": { "linux_fx_version": "NODE|10.14" + }, + "github_actions_properties": { + "github_actions_version": "10.14" } }, { "displayName": "JAVA|8-jre8", "configs": { "linux_fx_version": "JAVA|8-jre8" + }, + "github_actions_properties": { + "github_actions_version": "8" } }, { "displayName": "JAVA|11-java11", "configs": { "linux_fx_version": "JAVA|11-java11" + }, + "github_actions_properties": { + "github_actions_version": "11" } }, { "displayName": "TOMCAT|8.5-jre8", "configs": { "linux_fx_version": "TOMCAT|8.5-jre8" + }, + "github_actions_properties": { + "github_actions_version": "8" } }, { "displayName": "TOMCAT|9.0-jre8", "configs": { "linux_fx_version": "TOMCAT|9.0-jre8" + }, + "github_actions_properties": { + "github_actions_version": "8" } }, { "displayName": "TOMCAT|8.5-java11", "configs": { "linux_fx_version": "TOMCAT|8.5-java11" + }, + "github_actions_properties": { + "github_actions_version": "11" } }, { "displayName": "TOMCAT|9.0-java11", "configs": { "linux_fx_version": "TOMCAT|9.0-java11" + }, + "github_actions_properties": { + "github_actions_version": "11" } }, { @@ -314,18 +453,27 @@ "displayName": "PYTHON|3.8", "configs": { "linux_fx_version": "PYTHON|3.8" + }, + "github_actions_properties": { + "github_actions_version": "3.8" } }, { "displayName": "PYTHON|3.7", "configs": { "linux_fx_version": "PYTHON|3.7" + }, + "github_actions_properties": { + "github_actions_version": "3.7" } }, { "displayName": "PYTHON|3.6", "configs": { "linux_fx_version": "PYTHON|3.6" + }, + "github_actions_properties": { + "github_actions_version": "3.6" } }, { diff --git a/src/azure-cli/setup.py b/src/azure-cli/setup.py index 348e396c749..61a5eda6d80 100644 --- a/src/azure-cli/setup.py +++ b/src/azure-cli/setup.py @@ -140,6 +140,7 @@ 'jsondiff==1.2.0', 'packaging~=20.9', 'PyGithub==1.38', + 'PyNaCl==1.4.0', 'pytz==2019.1', 'scp~=0.13.2', 'semver==2.13.0', From a94a2ffcdc301cfdb8967fb3093888a98f9252bc Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 27 May 2021 08:47:55 -0700 Subject: [PATCH 2/8] Add --login-with-github flag to specify if you want to use interactive method to get github personal access token --- .../cli/command_modules/appservice/_help.py | 14 +++++++++ .../cli/command_modules/appservice/_params.py | 1 + .../cli/command_modules/appservice/custom.py | 30 +++++++++++-------- .../appservice/static_sites.py | 12 ++------ .../cli/command_modules/appservice/utils.py | 8 +++++ 5 files changed, 43 insertions(+), 22 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 7f60b5c62c2..7a25c0554f7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1718,11 +1718,25 @@ helps['webapp deployment github-actions add'] = """ type: command short-summary: Adds a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified webapp. + examples: + - name: Add GitHub Actions to a specified repository, providing personal access token + text: > + az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken + - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token + text: > + az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github """ helps['webapp deployment github-actions remove'] = """ type: command short-summary: Removes and disconnects the GitHub Actions workflow file from the specified repository. + examples: + - name: Remove GitHub Actions from a specified repository, providing personal access token + text: > + az webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken + - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token + text: > + az webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github """ helps['webapp hybrid-connection'] = """ 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 2b04179d5b8..10de9ed7514 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -419,6 +419,7 @@ def load_arguments(self, _): 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('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.') + c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token') with self.argument_context('webapp deployment github-actions add')as c: c.argument('runtime', options_list=['--runtime', '-r'], help='Canonicalized web runtime in the format of Framework|Version, e.g. "PHP|5.6". Use "az webapp list-runtimes" for available list.') 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 196bf7ae453..87956d81789 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -56,7 +56,7 @@ from ._params import AUTH_TYPES, MULTI_CONTAINER_TYPES from ._client_factory import web_client_factory, ex_handler_factory, providers_client_factory from ._appservice_utils import _generic_site_operation, _generic_settings_operation -from .utils import _normalize_sku, get_sku_name, retryable_method +from .utils import _normalize_sku, get_sku_name, retryable_method, raise_missing_token_suggestion from ._create_util import (zip_contents_from_dir, get_runtime_version_details, create_resource_group, get_app_details, should_create_new_rg, set_location, get_site_availability, get_profile_username, get_plan_to_use, get_lang_from_content, get_rg_to_use, get_sku_to_use, @@ -4465,11 +4465,14 @@ def delete_function_key(cmd, resource_group_name, name, key_name, function_name= return client.web_apps.delete_function_secret(resource_group_name, name, function_name, key_name) -def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, branch='master', force=False): - if not token: - token = get_github_access_token(cmd, ['admin:repo_hook', 'repo', 'workflow']) - if not token: - raise CLIError("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.") +def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, branch='master', 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") # Verify resource group, app site_availability = get_site_availability(cmd, name) @@ -4590,11 +4593,14 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None return github_actions_url -def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None, branch='master'): - if not token: - token = get_github_access_token(cmd, ['admin:repo_hook', 'repo', 'workflow']) - if not token: - raise CLIError("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.") +def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None, 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) @@ -4902,4 +4908,4 @@ def _encrypt_github_actions_secret(public_key, secret_value): public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) sealed_box = public.SealedBox(public_key) encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) - return b64encode(encrypted).decode("utf-8") \ No newline at end of file + return b64encode(encrypted).decode("utf-8") diff --git a/src/azure-cli/azure/cli/command_modules/appservice/static_sites.py b/src/azure-cli/azure/cli/command_modules/appservice/static_sites.py index 1cb2775abc1..acb54c3400a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/static_sites.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/static_sites.py @@ -8,7 +8,7 @@ from knack.util import CLIError from knack.log import get_logger -from .utils import normalize_sku_for_staticapp +from .utils import normalize_sku_for_staticapp, raise_missing_token_suggestion logger = get_logger(__name__) @@ -195,7 +195,7 @@ def create_staticsites(cmd, resource_group_name, name, location, app_location='.', api_location='.', output_location='.github/workflows', tags=None, no_wait=False, sku='Free', login_with_github=False): if not token and not login_with_github: - _raise_missing_token_suggestion() + raise_missing_token_suggestion() elif not token: from ._github_oauth import get_github_access_token scopes = ["admin:repo_hook", "repo", "workflow"] @@ -276,14 +276,6 @@ def _parse_pair(pair, delimiter): return pair[:index], pair[1 + index:] -def _raise_missing_token_suggestion(): - pat_documentation = "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line" - raise CLIError("GitHub access token is required to authenticate to your repositories. " - "If you need to create a Github Personal Access Token, " - "please run with the '--login-with-github' flag or follow " - "the steps found at the following link:\n{0}".format(pat_documentation)) - - def _get_staticsite_location(client, static_site_name, resource_group_name): static_sites = client.list() for static_site in static_sites: 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 a0e4caf91aa..7bf348397a2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -75,3 +75,11 @@ def call(*args, **kwargs): time.sleep(interval_sec) return call return decorate + + +def raise_missing_token_suggestion(): + pat_documentation = "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line" + raise CLIError("GitHub access token is required to authenticate to your repositories. " + "If you need to create a Github Personal Access Token, " + "please run with the '--login-with-github' flag or follow " + "the steps found at the following link:\n{0}".format(pat_documentation)) From d3eab694cd0d61fb64779477f8023ccb3e888f09 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 28 May 2021 09:18:28 -0700 Subject: [PATCH 3/8] Linter --- .../command_modules/appservice/_constants.py | 20 +- .../cli/command_modules/appservice/_help.py | 5 + .../cli/command_modules/appservice/custom.py | 208 +++++++++++------- 3 files changed, 139 insertions(+), 94 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 8902cb07381..104a62bc658 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -77,17 +77,17 @@ def __init__(self): PUBLIC_CLOUD = "AzureCloud" LINUX_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { - 'node': 'AppService/linux/nodejs-webapp-on-azure.yml', - 'python': 'AppService/linux/python-webapp-on-azure.yml', - 'dotnetcore': 'AppService/linux/aspnet-core-webapp-on-azure.yml', - 'java': 'AppService/linux/java-jar-webapp-on-azure.yml', - 'tomcat': 'AppService/linux/java-war-webapp-on-azure.yml' + 'node': 'AppService/linux/nodejs-webapp-on-azure.yml', + 'python': 'AppService/linux/python-webapp-on-azure.yml', + 'dotnetcore': 'AppService/linux/aspnet-core-webapp-on-azure.yml', + 'java': 'AppService/linux/java-jar-webapp-on-azure.yml', + 'tomcat': 'AppService/linux/java-war-webapp-on-azure.yml' } WINDOWS_GITHUB_ACTIONS_WORKFLOW_TEMPLATE_PATH = { - 'node': 'AppService/windows/nodejs-webapp-on-azure.yml', - 'python': 'AppService/windows/python-webapp-on-azure.yml', - 'dotnetcore': 'AppService/windows/aspnet-core-webapp-on-azure.yml', - 'java': 'AppService/windows/java-jar-webapp-on-azure.yml', - 'tomcat': 'AppService/windows/java-war-webapp-on-azure.yml' + 'node': 'AppService/windows/nodejs-webapp-on-azure.yml', + 'python': 'AppService/windows/python-webapp-on-azure.yml', + 'dotnetcore': 'AppService/windows/aspnet-core-webapp-on-azure.yml', + 'java': 'AppService/windows/java-jar-webapp-on-azure.yml', + 'tomcat': 'AppService/windows/java-war-webapp-on-azure.yml' } 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 7a25c0554f7..d2782d835a2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1715,6 +1715,11 @@ az webapp deployment user show """ +helps['webapp deployment github-actions'] = """ +type: group +short-summary: Configure GitHub Actions for a webapp +""" + helps['webapp deployment github-actions add'] = """ type: command short-summary: Adds a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified webapp. 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 87956d81789..3dbfacc9dee 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -11,7 +11,6 @@ from urllib.parse import urlparse except ImportError: from urlparse import urlparse # pylint: disable=import-error -from base64 import b64encode from binascii import hexlify from os import urandom import datetime @@ -4465,7 +4464,8 @@ def delete_function_key(cmd, resource_group_name, name, key_name, function_name= return client.web_apps.delete_function_secret(resource_group_name, name, function_name, key_name) -def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, branch='master', login_with_github=False, force=False): +def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None, slot=None, # pylint: disable=too-many-statements,too-many-branches + branch='master', login_with_github=False, force=False): if not token and not login_with_github: raise_missing_token_suggestion() elif not token: @@ -4476,15 +4476,21 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None # 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/{}' under resource group '{}' was not found.".format(name, resource_group)) + 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 {}. Please check that the app is a part of the current subscription".format(name)) + 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 {} exists in ResourceGroup {} and does not match the value entered {}. Please " - "re-run command with the correct parameters.".format(name, current_rg, resource_group)) + 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)) 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']) @@ -4493,7 +4499,6 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None # Verify github repo from github import Github, GithubException from github.GithubException import BadCredentialsException, UnknownObjectException - import yaml if repo.strip()[-1] == '/': repo = repo.strip()[:-1] @@ -4510,8 +4515,10 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None error_msg += " Error: {}".format(e.data['message']) raise CLIError(error_msg) logger.warning('Verified GitHub repo and branch') - except BadCredentialsException as e: - raise CLIError("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 BadCredentialsException: + raise CLIError("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']: @@ -4519,13 +4526,21 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None 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 = app_runtime_info['display_name'] if (app_runtime_info and app_runtime_info['display_name']) else None - github_actions_version = app_runtime_info['github_actions_version'] if (app_runtime_info and app_runtime_info['github_actions_version']) else None + 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: {} does not match the runtime specified: {}. Using the specified runtime {}.'.format(app_runtime_string, runtime, runtime)) + 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 @@ -4534,31 +4549,38 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None raise CLIError('Could not detect runtime. Please specify using the --runtime flag.') if not _runtime_supports_github_actions(runtime_string=app_runtime_string, is_linux=is_linux): - raise CLIError("Runtime {} is not supported for GitHub Actions deployments.".format(app_runtime_string)) + raise CLIError("Runtime %s is not supported for GitHub Actions deployments." % app_runtime_string) # Get workflow template - logger.warning('Getting workflow template using runtime: {}'.format(app_runtime_string)) + logger.warning('Getting workflow template using runtime: %s', app_runtime_string) workflow_template = _get_workflow_template(github=g, runtime_string=app_runtime_string, is_linux=is_linux) # Fill workflow template - import uuid guid = str(uuid.uuid4()).replace('-', '') publish_profile_name = "AzureAppService_PublishProfile_{}".format(guid) - logger.warning('Fill workflow template with name: {}, branch: {}, version: {}, slot: {}'.format(name, branch, github_actions_version, slot if slot else 'production')) + 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_workflow_template(content=workflow_template.decoded_content.decode(), name=name, - branch=branch, slot=slot, publish_profile=publish_profile_name, version=github_actions_version) + branch=branch, slot=slot, publish_profile=publish_profile_name, + version=github_actions_version) completed_workflow_file = completed_workflow_file.encode() # Check if workflow exists in repo, otherwise push - file_name = "{}_{}({}).yml".format(branch.replace('/', '-'), name.lower(), slot) if slot else "{}_{}.yml".format(branch.replace('/', '-'), name.lower()) + 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)) + 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.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") @@ -4571,29 +4593,33 @@ def add_github_actions(cmd, resource_group, name, repo, runtime=None, token=None 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) + 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 as e: - logger.warning("Creating new workflow file: {}".format(file_path)) + 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('Add 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) + 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) + _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_github_actions(cmd, resource_group, name, repo, token=None, slot=None, branch='master', login_with_github=False): +def remove_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: @@ -4604,20 +4630,23 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None # 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 CLIError("The Resource 'Microsoft.Web/sites/{}' under resource group '{}' was not found.".format(name, resource_group)) + if site_availability.name_available or (not site_availability.name_available and + site_availability.reason == 'Invalid'): + raise CLIError("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 CLIError("Unable to retrieve details of the existing app {}. Please check that the app is a part of the current subscription".format(name)) + raise CLIError("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 CLIError("The webapp {} exists in ResourceGroup {} and does not match the value entered {}. Please " - "re-run command with the correct parameters.".format(name, current_rg, resource_group)) + raise CLIError("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 - import yaml if repo.strip()[-1] == '/': repo = repo.strip()[:-1] @@ -4634,8 +4663,10 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None error_msg += " Error: {}".format(e.data['message']) raise CLIError(error_msg) logger.warning('Verified GitHub repo and branch') - except BadCredentialsException as e: - raise CLIError("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 BadCredentialsException: + raise CLIError("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']: @@ -4643,13 +4674,16 @@ def remove_github_actions(cmd, resource_group, name, repo, token=None, slot=None 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()) + 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)) + 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) @@ -4688,7 +4722,8 @@ def _get_publish_profile_from_workflow_file(workflow_file): return None -def _update_site_source_control_properties_for_gh_action(cmd, resource_group, name, token, repo=None, branch="master", slot=None): +def _update_site_source_control_properties_for_gh_action(cmd, resource_group, name, token, repo=None, + branch="master", slot=None): if repo: repo_url = 'https://github.com/' + repo else: @@ -4728,8 +4763,10 @@ def _get_workflow_template(github, runtime_string, is_linux): try: template_repo = github.get_repo(template_repo_path) file_contents = template_repo.get_contents(template_file_path) - except BadCredentialsException as e: - raise CLIError("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 BadCredentialsException: + raise CLIError("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 retrieving workflow template" if e.data and e.data['message']: @@ -4785,7 +4822,9 @@ def _add_publish_profile_to_github(cmd, resource_group, name, repo, token, githu import requests logger.warning("Fetching publish profile with secrets for the app '%s'", name) - publish_profile_bytes= _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_publishing_profile_xml_with_secrets', slot, {"format": "WebDeploy"}) + publish_profile_bytes = _generic_site_operation( + cmd.cli_ctx, resource_group, name, 'list_publishing_profile_xml_with_secrets', + slot, {"format": "WebDeploy"}) publish_profile = [x for x in publish_profile_bytes] if publish_profile: publish_profile = publish_profile[0].decode('ascii') @@ -4804,17 +4843,17 @@ def _add_publish_profile_to_github(cmd, resource_group, name, repo, token, githu raise CLIError('Request to GitHub for public key failed.') public_key = public_key.json() - encrypted_github_actions_secret = _encrypt_github_actions_secret(public_key=public_key['key'], secret_value=str(publish_profile)) + encrypted_github_actions_secret = _encrypt_github_actions_secret(public_key=public_key['key'], + secret_value=str(publish_profile)) payload = { "encrypted_value": encrypted_github_actions_secret, "key_id": public_key['key_id'] } - import json store_secret_url = "https://api.github.com/repos/{}/actions/secrets/{}".format(repo, github_actions_secret_name) stored_secret = requests.put(store_secret_url, data=json.dumps(payload), headers=headers) if str(stored_secret.status_code)[0] != '2': - raise CLIError('Unable to add publish profile to GitHub. Request status code: {}'.format(stored_secret.status_code)) + raise CLIError('Unable to add publish profile to GitHub. Request status code: %s' % stored_secret.status_code) def _remove_publish_profile_from_github(cmd, resource_group, name, repo, token, github_actions_secret_name, slot=None): @@ -4831,7 +4870,7 @@ def _runtime_supports_github_actions(runtime_string, is_linux): stacks = get_file_json(RUNTIME_STACKS)['linux'] else: stacks = get_file_json(RUNTIME_STACKS)['windows'] - + supports = False for stack in stacks: if stack['displayName'].lower() == runtime_string.lower(): @@ -4840,7 +4879,7 @@ def _runtime_supports_github_actions(runtime_string, is_linux): return supports -def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): # {'display_name': '', 'github_actions_version': ''} +def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): app_settings = None app_runtime = None @@ -4848,36 +4887,35 @@ def _get_app_runtime_info(cmd, resource_group, name, slot, is_linux): # {'displ 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_app_runtime_info_helper(app_runtime, "", is_linux) - else: - app_metadata = _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_metadata', slot) - app_metadata_properties = getattr(app_metadata, 'properties', {}) - if 'CURRENT_STACK' in app_metadata_properties: - app_runtime = app_metadata_properties['CURRENT_STACK'] - - 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_app_runtime_info_helper(app_runtime, app_runtime_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_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) - elif app_runtime and app_runtime.lower() == 'dotnetcore': - app_runtime_version = '3.1' - app_runtime_version = "" - return _get_app_runtime_info_helper(app_runtime, app_runtime_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}, {java_container}, {java_container_version}".format( - java_version=getattr(app_settings, 'java_version', '').lower(), - java_container=getattr(app_settings, 'java_container', '').lower(), - java_container_version=getattr(app_settings, 'java_container_version', '').lower() - ) - return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) - return None + + app_metadata = _generic_site_operation(cmd.cli_ctx, resource_group, name, 'list_metadata', slot) + app_metadata_properties = getattr(app_metadata, 'properties', {}) + if 'CURRENT_STACK' in app_metadata_properties: + app_runtime = app_metadata_properties['CURRENT_STACK'] + + 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_app_runtime_info_helper(app_runtime, app_runtime_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_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) + elif app_runtime and app_runtime.lower() == 'dotnetcore': + app_runtime_version = '3.1' + app_runtime_version = "" + return _get_app_runtime_info_helper(app_runtime, app_runtime_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}, {java_container}, {java_container_version}".format( + java_version=getattr(app_settings, 'java_version', '').lower(), + java_container=getattr(app_settings, 'java_container', '').lower(), + java_container_version=getattr(app_settings, 'java_container_version', '').lower() + ) + return _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux) def _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux): @@ -4895,16 +4933,18 @@ def _get_app_runtime_info_helper(app_runtime, app_runtime_version, is_linux): for stack in stacks: if 'github_actions_properties' in stack and stack['github_actions_properties']: if (stack['github_actions_properties']['app_runtime'].lower() == app_runtime.lower() and - stack['github_actions_properties']['app_runtime_version'].lower() == app_runtime_version.lower()): - return { - "display_name": stack['displayName'], - "github_actions_version": stack['github_actions_properties']['github_actions_version'] - } + stack['github_actions_properties']['app_runtime_version'].lower() == + app_runtime_version.lower()): + return { + "display_name": stack['displayName'], + "github_actions_version": stack['github_actions_properties']['github_actions_version'] + } return None def _encrypt_github_actions_secret(public_key, secret_value): # Encrypt a Unicode string using the public key + from base64 import b64encode public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) sealed_box = public.SealedBox(public_key) encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) From 9ef41bb7eff0782fc5e2b1f68263f0fb6ede894d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Jun 2021 15:54:43 -0700 Subject: [PATCH 4/8] Fixes --- src/azure-cli/azure/cli/command_modules/appservice/_help.py | 4 ++-- .../azure/cli/command_modules/appservice/_params.py | 5 ++--- .../azure/cli/command_modules/appservice/commands.py | 2 +- 3 files changed, 5 insertions(+), 6 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 d2782d835a2..85d0dc75afd 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1722,7 +1722,7 @@ helps['webapp deployment github-actions add'] = """ type: command - short-summary: Adds a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified webapp. + short-summary: Add a GitHub Actions workflow file to the specified repository. The workflow will build and deploy your app to the specified webapp. examples: - name: Add GitHub Actions to a specified repository, providing personal access token text: > @@ -1734,7 +1734,7 @@ helps['webapp deployment github-actions remove'] = """ type: command - short-summary: Removes and disconnects the GitHub Actions workflow file from the specified repository. + 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: > 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 10de9ed7514..c436e08bb69 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -418,12 +418,11 @@ def load_arguments(self, _): 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('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.') - c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token') + c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token', action='store_true') with self.argument_context('webapp deployment github-actions add')as c: c.argument('runtime', options_list=['--runtime', '-r'], help='Canonicalized web runtime in the format of Framework|Version, e.g. "PHP|5.6". Use "az webapp list-runtimes" for available list.') - c.argument('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.') + c.argument('force', options_list=['--force', '-f'], help='When true, the command will overwrite any workflow file with a conflicting name.', action='store_true') with self.argument_context('webapp log config') as c: c.argument('application_logging', help='configure application logging', 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 9d5838fa6f6..c177d132f44 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -247,7 +247,7 @@ 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', is_preview=True) as g: g.custom_command('add', 'add_github_actions') g.custom_command('remove', 'remove_github_actions') From 389ff6879304c6f9982a0a271319ccc53cac68e1 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Jun 2021 09:55:35 -0700 Subject: [PATCH 5/8] don't pin pynacl package --- src/azure-cli/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/setup.py b/src/azure-cli/setup.py index 61a5eda6d80..0cd7adfd236 100644 --- a/src/azure-cli/setup.py +++ b/src/azure-cli/setup.py @@ -140,7 +140,7 @@ 'jsondiff==1.2.0', 'packaging~=20.9', 'PyGithub==1.38', - 'PyNaCl==1.4.0', + 'PyNaCl~=1.4.0', 'pytz==2019.1', 'scp~=0.13.2', 'semver==2.13.0', From 927312cdc797267f5ed641567e6372c42461524f Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Jun 2021 11:23:36 -0700 Subject: [PATCH 6/8] Fix linter --- src/azure-cli/azure/cli/command_modules/appservice/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3dbfacc9dee..1065fe1517e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -4825,7 +4825,7 @@ def _add_publish_profile_to_github(cmd, resource_group, name, repo, token, githu publish_profile_bytes = _generic_site_operation( cmd.cli_ctx, resource_group, name, 'list_publishing_profile_xml_with_secrets', slot, {"format": "WebDeploy"}) - publish_profile = [x for x in publish_profile_bytes] + publish_profile = list(publish_profile_bytes) if publish_profile: publish_profile = publish_profile[0].decode('ascii') else: From d08072bbe61959354b08691dddee98fab0093f97 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 25 Jun 2021 10:40:06 -0700 Subject: [PATCH 7/8] Unpin PyGithub --- src/azure-cli/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/setup.py b/src/azure-cli/setup.py index 0cd7adfd236..916710c2478 100644 --- a/src/azure-cli/setup.py +++ b/src/azure-cli/setup.py @@ -139,7 +139,7 @@ 'jsmin~=2.2.2', 'jsondiff==1.2.0', 'packaging~=20.9', - 'PyGithub==1.38', + 'PyGithub~=1.38', 'PyNaCl~=1.4.0', 'pytz==2019.1', 'scp~=0.13.2', From 7832b3fa11faa290ba25f711326a3277bff40407 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 30 Jun 2021 14:47:24 -0700 Subject: [PATCH 8/8] Fixes --- .../cli/command_modules/appservice/_help.py | 36 +++++++++---------- .../cli/command_modules/appservice/utils.py | 9 ++--- 2 files changed, 23 insertions(+), 22 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 85d0dc75afd..ddf419d2393 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1721,27 +1721,27 @@ """ helps['webapp 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 webapp. - examples: - - name: Add GitHub Actions to a specified repository, providing personal access token - text: > - az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken - - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token - text: > - az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github +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 webapp. +examples: + - name: Add GitHub Actions to a specified repository, providing personal access token + text: > + az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken + - name: Add GitHub Actions to a specified repository, using interactive method of retrieving personal access token + text: > + az webapp deployment github-actions add --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github """ helps['webapp 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 webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken - - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token - text: > - az webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github +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 webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --token MyPersonalAccessToken + - name: Remove GitHub Actions from a specified repository, using interactive method of retrieving personal access token + text: > + az webapp deployment github-actions remove --repo "githubUser/githubRepo" -g MyResourceGroup -n MyWebapp --login-with-github """ helps['webapp hybrid-connection'] = """ 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 7bf348397a2..7a7e1afa76b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/utils.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/utils.py @@ -5,6 +5,7 @@ import time from knack.util import CLIError +from azure.cli.core.azclierror import (RequiredArgumentMissingError) def str2bool(v): @@ -79,7 +80,7 @@ def call(*args, **kwargs): def raise_missing_token_suggestion(): pat_documentation = "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line" - raise CLIError("GitHub access token is required to authenticate to your repositories. " - "If you need to create a Github Personal Access Token, " - "please run with the '--login-with-github' flag or follow " - "the steps found at the following link:\n{0}".format(pat_documentation)) + raise RequiredArgumentMissingError("GitHub access token is required to authenticate to your repositories. " + "If you need to create a Github Personal Access Token, " + "please run with the '--login-with-github' flag or follow " + "the steps found at the following link:\n{0}".format(pat_documentation))