Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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