Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[App Service] functionapp deployment github-actions: Add new functionapp github-actions commands #23326

Merged
merged 16 commits into from
Jul 28, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"repo",
"workflow"
]
LOGICAPP_KIND = "workflowapp"
FUNCTIONAPP_KIND = "functionapp"


class FUNCTIONS_STACKS_API_KEYS():
Expand All @@ -53,6 +55,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__),
Expand All @@ -75,3 +78,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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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')
29 changes: 29 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
runefa marked this conversation as resolved.
Show resolved Hide resolved
"""

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.
Expand Down
15 changes: 15 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com/<owner>/<repository-name> or <owner>/<repository-name>')
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.')
runefa marked this conversation as resolved.
Show resolved Hide resolved
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.')
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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.")
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -255,10 +256,14 @@ 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', validator=validate_app_is_functionapp) as g:
g.custom_command('add', 'add_functionapp_github_actions')
g.custom_command('remove', 'remove_functionapp_github_actions')
runefa marked this conversation as resolved.
Show resolved Hide resolved

with self.command_group('webapp auth') as g:
g.custom_show_command('show', 'get_auth_settings')
g.custom_command('update', 'update_auth_settings')
Expand Down
Loading