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
20 changes: 20 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 @@ -931,6 +931,16 @@
text: az functionapp vnet-integration remove -g MyResourceGroup -n MyFunctionapp -s [slot]
"""

helps['functionapp deploy'] = """
type: command
short-summary: Deploys a provided artifact to Azure functionapp.
examples:
- name: Deploy a war file asynchronously.
text: az functionapp deploy --resource-group ResouceGroup --name AppName --src-path SourcePath --type war --async true
- name: Deploy a static text file to wwwroot/staticfiles/test.txt
text: az functionapp deploy --resource-group ResouceGroup --name AppName --src-path SourcePath --type static --target-path staticfiles/test.txt
"""

helps['webapp'] = """
type: group
short-summary: Manage web apps.
Expand Down Expand Up @@ -2429,3 +2439,13 @@
- name: Updates a user entry with the listed roles.
text: az staticwebapp users update -n MyStaticAppName --user-details JohnDoe --role Contributor
"""

helps['webapp deploy'] = """
type: command
short-summary: Deploys a provided artifact to Azure Web Apps.
examples:
- name: Deploy a war file asynchronously.
text: az webapp deploy --resource-group ResouceGroup --name AppName --src-path SourcePath --type war --async IsAsync
- name: Deploy a static text file to wwwroot/staticfiles/test.txt
text: az webapp deploy --resource-group ResouceGroup --name AppName --src-path SourcePath --type static --target-path staticfiles/test.txt
"""
26 changes: 26 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 @@ -643,6 +643,32 @@ def load_arguments(self, _):
c.argument('subnet', help="The name of the subnet",
local_context_attribute=LocalContextAttribute(name='subnet_name', actions=[LocalContextAction.GET]))

with self.argument_context('webapp deploy') as c:
c.argument('name', options_list=['--name', '-n'], help='Name of the webapp to connect to')
c.argument('src_path', options_list=['--src-path'], help='Path of the file to be deployed. Example: /mnt/apps/myapp.war')
c.argument('src_url', options_list=['--src-url'], help='url to download the package from. Example: http://mysite.com/files/myapp.war?key=123')
c.argument('target_path', options_list=['--target-path'], help='Target path relative to wwwroot to which the file will be deployed to.')
c.argument('artifact_type', options_list=['--type'], help='Type of deployment requested')
c.argument('is_async', options_list=['--async'], help='Asynchronous deployment', choices=['true', 'false'])
c.argument('restart', options_list=['--restart'], help='restart or not. default behavior is to restart.', choices=['true', 'false'])
c.argument('clean', options_list=['--clean'], help='clean or not. default is target-type specific.', choices=['true', 'false'])
c.argument('ignore_stack', options_list=['--ignore-stack'], help='should override the default stack check', choices=['true', 'false'])
c.argument('timeout', options_list=['--timeout'], help='Timeout for operation in milliseconds')
c.argument('slot', help="Name of the deployment slot to use")

with self.argument_context('functionapp deploy') as c:
c.argument('name', options_list=['--name', '-n'], help='Name of the functionapp to connect to')
c.argument('src_path', options_list=['--src-path'], help='Path of the file to be deployed. Example: /mnt/apps/myapp.war')
c.argument('src_url', options_list=['--src-url'], help='url to download the package from. Example: http://mysite.com/files/myapp.war?key=123')
c.argument('target_path', options_list=['--target-path'], help='Target path relative to wwwroot to which the file will be deployed to.')
c.argument('artifact_type', options_list=['--type'], help='Type of deployment requested')
c.argument('is_async', options_list=['--async'], help='Asynchronous deployment', choices=['true', 'false'])
c.argument('restart', options_list=['--restart'], help='restart or not. default behavior is to restart.', choices=['true', 'false'])
c.argument('clean', options_list=['--clean'], help='clean or not. default is target-type specific.', choices=['true', 'false'])
c.argument('ignore_stack', options_list=['--ignore-stack'], help='should override the default stack check', choices=['true', 'false'])
c.argument('timeout', options_list=['--timeout'], help='Timeout for operation in milliseconds')
c.argument('slot', help="Name of the deployment slot to use")

with self.argument_context('functionapp vnet-integration') as c:
c.argument('name', arg_type=functionapp_name_arg_type, id_part=None)
c.argument('slot', help="The name of the slot. Default to the productions slot if not specified")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ def validate_ip_address(cmd, namespace):
_validate_ip_address_existence(cmd, namespace)


def validate_onedeploy_params(namespace):
if namespace.src_path and namespace.src_url:
raise CLIError('Only one of --src-path and --src-url can be specified')

if not namespace.src_path and not namespace.src_url:
raise CLIError('Either of --src-path or --src-url must be specified')

if namespace.src_url and not namespace.artifact_type:
raise CLIError('Deployment type is mandatory when deploying from URLs. Use --type')


def _validate_ip_address_format(namespace):
if namespace.ip_address is not None:
# IPv6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from azure.cli.core.util import empty_on_404

from ._client_factory import cf_web_client, cf_plans, cf_webapps
from ._validators import validate_app_exists_in_rg, validate_app_or_slot_exists_in_rg, validate_asp_sku
from ._validators import validate_app_exists_in_rg, validate_app_or_slot_exists_in_rg, validate_asp_sku, validate_onedeploy_params


def output_slots_in_table(slots):
Expand Down Expand Up @@ -129,6 +129,7 @@ def load_command_table(self, _):
g.custom_show_command('identity show', 'show_identity', validator=validate_app_or_slot_exists_in_rg)
g.custom_command('identity remove', 'remove_identity', validator=validate_app_or_slot_exists_in_rg)
g.custom_command('create-remote-connection', 'create_tunnel', exception_handler=ex_handler_factory())
g.custom_command('deploy', 'perform_onedeploy', validator=validate_onedeploy_params, is_preview=True)
g.generic_update_command('update', getter_name='get_webapp', setter_name='set_webapp', custom_func_name='update_webapp', command_type=appservice_custom)

with self.command_group('webapp traffic-routing') as g:
Expand Down Expand Up @@ -305,6 +306,7 @@ def load_command_table(self, _):
g.custom_command('identity assign', 'assign_identity')
g.custom_show_command('identity show', 'show_identity')
g.custom_command('identity remove', 'remove_identity')
g.custom_command('deploy', 'perform_onedeploy', validator=validate_onedeploy_params, is_preview=True)
g.generic_update_command('update', setter_name='set_functionapp', exception_handler=update_function_ex_handler_factory(),
custom_func_name='update_functionapp', setter_type=appservice_custom, command_type=webapp_sdk)

Expand Down
202 changes: 202 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -3986,6 +3986,208 @@ def create_tunnel_and_session(cmd, resource_group_name, name, port=None, slot=No
time.sleep(5)


def perform_onedeploy(cmd,
resource_group_name,
name,
src_path=None,
src_url=None,
target_path=None,
artifact_type=None,
is_async=None,
restart=None,
clean=None,
ignore_stack=None,
timeout=None,
slot=None):
params = OneDeployParams()

params.cmd = cmd
params.resource_group_name = resource_group_name
params.webapp_name = name
params.src_path = src_path
params.src_url = src_url
params.target_path = target_path
params.artifact_type = artifact_type
params.is_async_deployment = is_async
params.should_restart = restart
params.is_clean_deployment = clean
params.should_ignore_stack = ignore_stack
params.timeout = timeout
params.slot = slot

return _perform_onedeploy_internal(params)


# Class for OneDeploy parameters
# pylint: disable=too-many-instance-attributes,too-few-public-methods
class OneDeployParams:
def __init__(self):
self.cmd = None
self.resource_group_name = None
self.webapp_name = None
self.src_path = None
self.src_url = None
self.artifact_type = None
self.is_async_deployment = None
self.target_path = None
self.should_restart = None
self.is_clean_deployment = None
self.should_ignore_stack = None
self.timeout = None
self.slot = None
# pylint: enable=too-many-instance-attributes,too-few-public-methods


def _build_onedeploy_url(params):
scm_url = _get_scm_url(params.cmd, params.resource_group_name, params.webapp_name, params.slot)
deploy_url = scm_url + '/api/publish?type=' + params.artifact_type

if params.is_async_deployment is not None:
deploy_url = deploy_url + '&async=' + str(params.is_async_deployment)

if params.should_restart is not None:
deploy_url = deploy_url + '&restart=' + str(params.should_restart)

if params.is_clean_deployment is not None:
deploy_url = deploy_url + '&clean=' + str(params.is_clean_deployment)

if params.should_ignore_stack is not None:
deploy_url = deploy_url + '&ignorestack=' + str(params.should_ignore_stack)

if params.target_path is not None:
deploy_url = deploy_url + '&path=' + params.target_path

return deploy_url


def _get_onedeploy_status_url(params):
scm_url = _get_scm_url(params.cmd, params.resource_group_name, params.webapp_name, params.slot)
return scm_url + '/api/deployments/latest'


def _get_basic_headers(params):
import urllib3

user_name, password = _get_site_credential(params.cmd.cli_ctx, params.resource_group_name,
params.webapp_name, params.slot)

if params.src_path:
content_type = 'application/octet-stream'
elif params.src_url:
content_type = 'application/json'
else:
raise CLIError('Unable to determine source location of the artifact being deployed')

headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(user_name, password))
headers['Cache-Control'] = 'no-cache'
headers['User-Agent'] = get_az_user_agent()
headers['Content-Type'] = content_type

return headers


def _get_onedeploy_request_body(params):
import os

if params.src_path:
logger.info('Deploying from local path: %s', params.src_path)
try:
with open(os.path.realpath(os.path.expanduser(params.src_path)), 'rb') as fs:
body = fs.read()
except Exception as e:
raise CLIError("Either '{}' is not a valid local file path or you do not have permissions to access it"
.format(params.src_path)) from e
elif params.src_url:
logger.info('Deploying from URL: %s', params.src_url)
body = json.dumps({
"packageUri": params.src_url
})
else:
raise CLIError('Unable to determine source location of the artifact being deployed')

return body


def _update_artifact_type(params):
import ntpath

if params.artifact_type is not None:
return

# Interpret deployment type from the file extension if the type parameter is not passed
file_name = ntpath.basename(params.src_path)
file_extension = file_name.split(".", 1)[1]
if file_extension in ('war', 'jar', 'ear', 'zip'):
params.artifact_type = file_extension
elif file_extension in ('sh', 'bat'):
params.artifact_type = 'startup'
else:
params.artifact_type = 'static'
logger.warning("Deployment type: %s. To override deloyment type, please specify the --type parameter. "
"Possible values: war, jar, ear, zip, startup, script, static", params.artifact_type)


def _make_onedeploy_request(params):
import requests

from azure.cli.core.util import (
should_disable_connection_verify,
)

# Build the request body, headers, API URL and status URL
body = _get_onedeploy_request_body(params)
headers = _get_basic_headers(params)
deploy_url = _build_onedeploy_url(params)
deployment_status_url = _get_onedeploy_status_url(params)

logger.info("Deployment API: %s", deploy_url)
response = requests.post(deploy_url, data=body, headers=headers, verify=not should_disable_connection_verify())

# For debugging purposes only, you can change the async deployment into a sync deployment by polling the API status
# For that, set poll_async_deployment_for_debugging=True
poll_async_deployment_for_debugging = True

# check the status of async deployment
if response.status_code == 202:
response_body = None
if poll_async_deployment_for_debugging:
logger.info('Polloing the status of async deployment')
response_body = _check_zip_deployment_status(params.cmd, params.resource_group_name, params.webapp_name,
deployment_status_url, headers, params.timeout)
logger.info('Async deployment complete. Server response: %s', response_body)
return response_body

if response.status_code == 200:
return response

# API not available yet!
if response.status_code == 404:
raise CLIError("This API isn't available in this environment yet!")

# check if there's an ongoing process
if response.status_code == 409:
raise CLIError("Another deployment is in progress. You can track the ongoing deployment at {}"
.format(deployment_status_url))

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


# OneDeploy
def _perform_onedeploy_internal(params):

# Update artifact type, if required
_update_artifact_type(params)

# Now make the OneDeploy API call
logger.info("Initiating deployment")
response = _make_onedeploy_request(params)
logger.info("Deployment has completed successfully")
return response


def _wait_for_webapp(tunnel_server):
tries = 0
while True:
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3256,5 +3256,24 @@ def test_functionapp_local_context(self, resource_group, storage_account):
self.cmd('functionapp plan delete -n {plan_name} -y')


class WebappOneDeployScenarioTest(ScenarioTest):
@live_only()
@ResourceGroupPreparer(name_prefix='cli_test_webapp_OneDeploy', location=WINDOWS_ASP_LOCATION_WEBAPP)
def test_one_deploy(self, resource_group):
webapp_name = self.create_random_name('webapp-oneDeploy-test', 40)
plan_name = self.create_random_name('webapp-oneDeploy-plan', 40)
war_file = os.path.join(TEST_DIR, 'sample.war')
self.cmd(
'appservice plan create -g {} -n {} --sku S1 --is-linux'.format(resource_group, plan_name))
self.cmd(
'webapp create -g {} -n {} --plan {} -r "TOMCAT|9.0-java11"'.format(resource_group, webapp_name, plan_name))
self.cmd('webapp deploy -g {} --n {} --src-path "{}" --type war --async true'.format(resource_group, webapp_name, war_file)).assert_with_checks([
JMESPathCheck('status', 4),
JMESPathCheck('deployer', 'OneDeploy'),
JMESPathCheck('message', 'OneDeploy'),
JMESPathCheck('complete', True)
])


if __name__ == '__main__':
unittest.main()