Skip to content

Commit

Permalink
Add zip deployment support
Browse files Browse the repository at this point in the history
Add workarounds for unsupported APIs

Add unit test

Fix unit tests
  • Loading branch information
kamperiadis authored and kaibocai committed May 24, 2023
1 parent 3e6393e commit 0f01b20
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 3 deletions.
54 changes: 51 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
_normalize_location,
get_pool_manager, use_additional_properties, get_app_service_plan_from_webapp,
get_resource_if_exists, repo_url_to_name, get_token,
app_service_plan_exists, is_centauri_functionapp)
app_service_plan_exists, is_centauri_functionapp, is_flex_functionapp)
from ._create_util import (zip_contents_from_dir, get_runtime_version_details, create_resource_group, get_app_details,
check_resource_group_exists, set_location, get_site_availability, get_profile_username,
get_plan_to_use, get_lang_from_content, get_rg_to_use, get_sku_to_use,
Expand Down Expand Up @@ -620,6 +620,9 @@ def enable_zip_deploy_functionapp(cmd, resource_group_name, name, src, build_rem
if is_consumption and app.reserved:
validate_zip_deploy_app_setting_exists(cmd, resource_group_name, name, slot)

if is_flex_functionapp(cmd, resource_group_name, name):
return enable_zip_deploy_flex(cmd, resource_group_name, name, src, timeout, slot, build_remote)

if (not build_remote) and is_consumption and app.reserved:
return upload_zip_to_storage(cmd, resource_group_name, name, src, slot)
if build_remote and app.reserved:
Expand All @@ -634,6 +637,49 @@ def enable_zip_deploy_webapp(cmd, resource_group_name, name, src, timeout=None,
return enable_zip_deploy(cmd, resource_group_name, name, src, timeout=timeout, slot=slot)


def enable_zip_deploy_flex(cmd, resource_group_name, name, src, timeout=None, slot=None, build_remote=False):
logger.warning("Getting scm site credentials for zip deployment")

try:
scm_url = _get_scm_url(cmd, resource_group_name, name, slot)
except ValueError:
raise ResourceNotFoundError('Failed to fetch scm url for function app')

zip_url = scm_url + '/api/Deploy/Zip?RemoteBuild={}&Deployer=az_cli'.format(build_remote)
deployment_status_url = scm_url + '/api/deployments/latest'

additional_headers = {"Content-Type": "application/zip", "Cache-Control": "no-cache"}
headers = get_scm_site_headers(cmd.cli_ctx, name, resource_group_name, slot,
additional_headers=additional_headers)

import os
import requests
from azure.cli.core.util import should_disable_connection_verify
# Read file content

with open(os.path.realpath(os.path.expanduser(src)), 'rb') as fs:
zip_content = fs.read()
logger.warning("Starting zip deployment. This operation can take a while to complete ...")
res = requests.post(zip_url, data=zip_content, headers=headers, verify=not should_disable_connection_verify())
logger.warning("Deployment endpoint responded with status code %d", res.status_code)

# check the status of async deployment
if res.status_code == 202:
response = _check_zip_deployment_status(cmd, resource_group_name, name, deployment_status_url,
headers, timeout)
return response

# check if there's an ongoing process
if res.status_code == 409:
raise UnclassifiedUserFault("There may be an ongoing deployment. Please track your deployment in {}"
.format(deployment_status_url))

# check if an error occured during deployment
if res.status_code:
raise AzureInternalError("An error occured during deployment. Status Code: {}, Details: {}"
.format(res.status_code, res.text))


def enable_zip_deploy(cmd, resource_group_name, name, src, timeout=None, slot=None):
logger.warning("Getting scm site credentials for zip deployment")

Expand Down Expand Up @@ -4334,7 +4380,8 @@ def _check_zip_deployment_status(cmd, rg_name, name, deployment_status_url, head
num_trials = num_trials + 1

if res_dict.get('status', 0) == 3:
_configure_default_logging(cmd, rg_name, name)
if not is_flex_functionapp(cmd, rg_name, name):
_configure_default_logging(cmd, rg_name, name)
raise CLIError("Zip deployment failed. {}. Please run the command az webapp log deployment show "
"-n {} -g {}".format(res_dict, name, rg_name))
if res_dict.get('status', 0) == 4:
Expand All @@ -4343,7 +4390,8 @@ def _check_zip_deployment_status(cmd, rg_name, name, deployment_status_url, head
logger.info(res_dict['progress']) # show only in debug mode, customers seem to find this confusing
# if the deployment is taking longer than expected
if res_dict.get('status', 0) != 4:
_configure_default_logging(cmd, rg_name, name)
if not is_flex_functionapp(cmd, rg_name, name):
_configure_default_logging(cmd, rg_name, name)
raise CLIError("""Timeout reached by the command, however, the deployment operation
is still on-going. Navigate to your scm site to check the deployment status""")
return res_dict
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from azure.cli.command_modules.appservice.custom import (
enable_zip_deploy_functionapp,
enable_zip_deploy,
enable_zip_deploy_flex,
add_remote_build_app_settings,
remove_remote_build_app_settings,
validate_app_settings_in_scm)
Expand Down Expand Up @@ -50,11 +51,13 @@ def setUp(self):

@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
@mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
@mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
@mock.patch('azure.cli.command_modules.appservice.custom.add_remote_build_app_settings')
def test_functionapp_zip_deploy_flow(self,
add_remote_build_app_settings_mock,
enable_zip_deploy_mock,
is_flex_functionapp_mock,
parse_resource_id_mock,
web_client_factory_mock):
cmd_mock = _get_test_cmd()
Expand All @@ -71,11 +74,13 @@ def test_functionapp_zip_deploy_flow(self,

@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
@mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
@mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
@mock.patch('azure.cli.command_modules.appservice.custom.remove_remote_build_app_settings')
def test_functionapp_zip_deploy_flow(self,
remove_remote_build_app_settings_mock,
enable_zip_deploy_mock,
is_flex_functionapp_mock,
parse_resource_id_mock,
web_client_factory_mock):
cmd_mock = _get_test_cmd()
Expand All @@ -95,7 +100,9 @@ def test_functionapp_zip_deploy_flow(self,
@mock.patch('azure.cli.command_modules.appservice.custom.validate_zip_deploy_app_setting_exists')
@mock.patch('azure.cli.command_modules.appservice.custom.upload_zip_to_storage')
@mock.patch('azure.cli.command_modules.appservice.custom.is_plan_consumption', return_value=True)
@mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
def test_functionapp_linux_consumption_non_remote_build(self,
is_flex_functionapp_mock,
is_plan_consumption_mock,
upload_zip_to_storage_mock,
validate_zip_deploy_app_setting_exists_mock,
Expand Down Expand Up @@ -127,7 +134,9 @@ def test_functionapp_linux_consumption_non_remote_build(self,
@mock.patch('azure.cli.command_modules.appservice.custom.validate_zip_deploy_app_setting_exists')
@mock.patch('azure.cli.command_modules.appservice.custom.upload_zip_to_storage')
@mock.patch('azure.cli.command_modules.appservice.custom.is_plan_consumption', return_value=True)
@mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
def test_functionapp_linux_consumption_non_remote_build_with_slot(self,
is_flex_functionapp_mock,
is_plan_consumption_mock,
upload_zip_to_storage_mock,
validate_zip_deploy_app_setting_exists_mock,
Expand Down Expand Up @@ -157,9 +166,11 @@ def test_functionapp_linux_consumption_non_remote_build_with_slot(self,
@mock.patch('azure.cli.command_modules.appservice.custom.add_remote_build_app_settings')
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
@mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
@mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
def test_functionapp_remote_build_supports_linux(self,
enable_zip_deploy_mock,
is_flex_functionapp_mock,
parse_resource_id_mock,
web_client_factory_mock,
add_remote_build_app_settings_mock):
Expand All @@ -183,6 +194,39 @@ def test_functionapp_remote_build_supports_linux(self,
web_client_mock.web_apps.get.assert_called_with('rg', 'name')
enable_zip_deploy_mock.assert_called_with(cmd_mock, 'rg', 'name', 'src', None, None)

@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
@mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', return_value='https://mock-scm')
@mock.patch('requests.post', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._check_zip_deployment_status')
def test_enable_zip_deploy_flex(self,
check_zip_deployment_status_mock,
requests_post_mock,
get_scm_url_mock,
get_scm_headers_mock):
# prepare
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cmd_mock.cli_ctx = cli_ctx_mock

response = mock.MagicMock()
response.status_code = 202
requests_post_mock.return_value = response

expected_zip_deploy_headers = _get_zip_deploy_headers('usr', 'pwd', cmd_mock.cli_ctx)
get_scm_headers_mock.return_value = expected_zip_deploy_headers

# action
with mock.patch('builtins.open', new_callable=mock.mock_open, read_data='zip-content'):
enable_zip_deploy_flex(cmd_mock, 'rg', 'name', 'src', slot=None, build_remote=True)

# assert
requests_post_mock.assert_called_with('https://mock-scm/api/Deploy/Zip?RemoteBuild=True&Deployer=az_cli', data='zip-content',
headers=expected_zip_deploy_headers, verify=mock.ANY)
# TODO improve authorization matcher
check_zip_deployment_status_mock.assert_called_with(cmd_mock, 'rg', 'name',
'https://mock-scm/api/deployments/latest', mock.ANY, None)


@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
@mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', side_effect=ValueError())
def test_enable_zip_deploy_remote_build_no_scm_site(self,
Expand Down
12 changes: 12 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ def is_centauri_functionapp(cmd, resource_group, name):
return function_app.get("properties", {}).get("managedEnvironmentId", None) is not None


def is_flex_functionapp(cmd, resource_group, name):
client = web_client_factory(cmd.cli_ctx)
app = client.web_apps.get(resource_group, name)
parse_plan_id = parse_resource_id(app.server_farm_id)
plan_info = client.app_service_plans.get(parse_plan_id['resource_group'], parse_plan_id['name'])
SkuDescription, AppServicePlan = cmd.get_models('SkuDescription', 'AppServicePlan')
if isinstance(plan_info, AppServicePlan):
if isinstance(plan_info.sku, SkuDescription):
return plan_info.sku.tier.lower() == 'flexconsumption'
return False


def _list_app(cli_ctx, resource_group_name=None):
client = web_client_factory(cli_ctx)
if resource_group_name:
Expand Down

0 comments on commit 0f01b20

Please sign in to comment.