Skip to content

Commit

Permalink
[App Service] functionapp deployment github-actions: Add new functi…
Browse files Browse the repository at this point in the history
…onapp github-actions commands (#23326)
  • Loading branch information
runefa committed Jul 28, 2022
1 parent 9625625 commit 2ad25b0
Show file tree
Hide file tree
Showing 8 changed files with 625 additions and 14 deletions.
17 changes: 17 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_constants.py
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
"""

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.')
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')

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

0 comments on commit 2ad25b0

Please sign in to comment.