Skip to content
Closed
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
58 changes: 53 additions & 5 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9856,12 +9856,60 @@ def _make_onedeploy_request(params):
deployment_status_url, params.slot, params.timeout)
logger.info('Server response: %s', response_body)
else:
# For --src-url deployments using ARM endpoint
if 'application/json' in response.headers.get('content-type', ""):
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
logger.warning("Deployment has completed successfully")
# Check if we should poll for completion (default is sync to match --src-path)
if params.is_async_deployment is not True:
# Try to extract deployment ID from ARM response
deployment_id = None
try:
response_json = response.json()
# Check for deployment ID in response
if 'id' in response_json:
deployment_id = response_json['id'].split('/')[-1]
elif 'properties' in response_json and 'deploymentId' in response_json['properties']:
deployment_id = response_json['properties']['deploymentId']
except Exception as ex: # pylint: disable=broad-except
logger.info("Failed to parse ARM response for deployment ID: %s", ex)

# If we have a deployment ID, poll for completion
if deployment_id:
logger.info("Tracking deployment ID: %s", deployment_id)
try:
deploymentstatusapi_url = _build_deploymentstatus_url(
params.cmd, params.resource_group_name, params.webapp_name,
params.slot, deployment_id
)
# Poll deployment status using the ARM deployment status API
logger.warning('Polling the status of sync deployment. Start Time: %s UTC',
datetime.datetime.now(datetime.timezone.utc))
response_body = _poll_deployment_runtime_status(
params.cmd, params.resource_group_name, params.webapp_name,
params.slot, deploymentstatusapi_url, deployment_id, params.timeout
)
except Exception as ex: # pylint: disable=broad-except
logger.warning("Failed to track deployment status: %s. "
"Deployment may still be in progress.", ex)
# Fallback to immediate response
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
else:
# No deployment ID found, return immediate response
logger.info("Could not extract deployment ID from ARM response, returning immediate status")
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
else:
# Async mode: return immediately with current state
state = response.json().get("properties", {}).get("provisioningState")
if state:
logger.warning("Deployment status is: \"%s\"", state)
response_body = response.json().get("properties", {})
if params.is_async_deployment is not True:
logger.warning("Deployment has completed successfully")
logger.warning("You can visit your app at: %s", _get_url(params.cmd, params.resource_group_name,
params.webapp_name, params.slot))
return response_body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,5 +644,292 @@ def __init__(self, status_code):
self.status_code = status_code


class TestWebappDeployWithSrcUrl(unittest.TestCase):
"""Tests for webapp deploy with --src-url sync/async behavior"""

@mock.patch('azure.cli.command_modules.appservice.custom._poll_deployment_runtime_status')
@mock.patch('azure.cli.command_modules.appservice.custom._build_deploymentstatus_url')
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
@mock.patch('azure.cli.command_modules.appservice.custom._get_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_request_body')
@mock.patch('azure.cli.command_modules.appservice.custom._build_onedeploy_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_status_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_ondeploy_headers')
def test_src_url_sync_deployment_default(self, headers_mock, status_url_mock, build_url_mock_onedeploy,
body_mock, get_url_mock, send_raw_mock, build_url_mock, poll_mock):
"""Test that --src-url defaults to sync deployment (polls for completion)"""
from azure.cli.command_modules.appservice.custom import _make_onedeploy_request

# Mock helper functions
body_mock.return_value = ('{"type": "zip"}', None)
build_url_mock_onedeploy.return_value = 'https://management.azure.com/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy'
status_url_mock.return_value = 'https://myapp.scm.azurewebsites.net/api/deployments/latest'
headers_mock.return_value = {'Content-Type': 'application/json'}

# Mock the ARM response with deployment ID
class MockResponse:
status_code = 200
headers = {'content-type': 'application/json'}
text = '{"id": "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy/123456"}'

def json(self):
return {
'id': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy/123456',
'properties': {'provisioningState': 'InProgress'}
}

send_raw_mock.return_value = MockResponse()
build_url_mock.return_value = 'https://management.azure.com/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/deploymentStatus/123456'
poll_mock.return_value = {'status': 'RuntimeSuccessful'}
get_url_mock.return_value = 'https://myapp.azurewebsites.net'

# Create params object
class Params:
src_url = 'https://example.com/myapp.zip'
src_path = None
is_async_deployment = None # Default should be sync
cmd = _get_test_cmd()
resource_group_name = 'test-rg'
webapp_name = 'test-app'
slot = None
timeout = None
is_linux_webapp = False
is_functionapp = False
enable_kudu_warmup = False

params = Params()

# Execute
result = _make_onedeploy_request(params)

# Assert polling was called
poll_mock.assert_called_once()
# Verify deployment ID was extracted correctly
build_url_mock.assert_called_with(params.cmd, 'test-rg', 'test-app', None, '123456')

@mock.patch('azure.cli.command_modules.appservice.custom._poll_deployment_runtime_status')
@mock.patch('azure.cli.command_modules.appservice.custom._build_deploymentstatus_url')
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
@mock.patch('azure.cli.command_modules.appservice.custom._get_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_request_body')
@mock.patch('azure.cli.command_modules.appservice.custom._build_onedeploy_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_status_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_ondeploy_headers')
def test_src_url_sync_deployment_explicit_false(self, headers_mock, status_url_mock, build_url_mock_onedeploy,
body_mock, get_url_mock, send_raw_mock, build_url_mock, poll_mock):
"""Test that --src-url with --async false triggers polling"""
from azure.cli.command_modules.appservice.custom import _make_onedeploy_request

# Mock helper functions
body_mock.return_value = ('{"type": "zip"}', None)
build_url_mock_onedeploy.return_value = 'https://management.azure.com/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy'
status_url_mock.return_value = 'https://myapp.scm.azurewebsites.net/api/deployments/latest'
headers_mock.return_value = {'Content-Type': 'application/json'}

# Mock the ARM response with deployment ID in properties
class MockResponse:
status_code = 200
headers = {'content-type': 'application/json'}
text = '{"properties": {"deploymentId": "dep-789"}}'

def json(self):
return {
'properties': {'deploymentId': 'dep-789', 'provisioningState': 'InProgress'}
}

send_raw_mock.return_value = MockResponse()
build_url_mock.return_value = 'https://management.azure.com/.../deploymentStatus/dep-789'
poll_mock.return_value = {'status': 'RuntimeSuccessful'}
get_url_mock.return_value = 'https://myapp.azurewebsites.net'

# Create params object with explicit async=false
class Params:
src_url = 'https://example.com/myapp.zip'
src_path = None
is_async_deployment = False # Explicitly set to False
cmd = _get_test_cmd()
resource_group_name = 'test-rg'
webapp_name = 'test-app'
slot = None
timeout = None
is_linux_webapp = False
is_functionapp = False
enable_kudu_warmup = False

params = Params()

# Execute
result = _make_onedeploy_request(params)

# Assert polling was called
poll_mock.assert_called_once()
build_url_mock.assert_called_with(params.cmd, 'test-rg', 'test-app', None, 'dep-789')

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
@mock.patch('azure.cli.command_modules.appservice.custom._get_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_request_body')
@mock.patch('azure.cli.command_modules.appservice.custom._build_onedeploy_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_status_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_ondeploy_headers')
def test_src_url_async_deployment(self, headers_mock, status_url_mock, build_url_mock_onedeploy,
body_mock, get_url_mock, send_raw_mock):
"""Test that --src-url with --async true returns immediately without polling"""
from azure.cli.command_modules.appservice.custom import _make_onedeploy_request

# Mock helper functions
body_mock.return_value = ('{"type": "zip"}', None)
build_url_mock_onedeploy.return_value = 'https://management.azure.com/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy'
status_url_mock.return_value = 'https://myapp.scm.azurewebsites.net/api/deployments/latest'
headers_mock.return_value = {'Content-Type': 'application/json'}

# Mock the ARM response
class MockResponse:
status_code = 200
headers = {'content-type': 'application/json'}
text = '{"id": "/subscriptions/sub/.../123456"}'

def json(self):
return {
'id': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy/123456',
'properties': {'provisioningState': 'InProgress'}
}

send_raw_mock.return_value = MockResponse()
get_url_mock.return_value = 'https://myapp.azurewebsites.net'

# Create params object with async=true
class Params:
src_url = 'https://example.com/myapp.zip'
src_path = None
is_async_deployment = True # Async mode
cmd = _get_test_cmd()
resource_group_name = 'test-rg'
webapp_name = 'test-app'
slot = None
timeout = None
is_linux_webapp = False
is_functionapp = False
enable_kudu_warmup = False

params = Params()

# Execute
result = _make_onedeploy_request(params)

# Assert result is immediate response (provisioningState returned)
self.assertEqual(result.get('provisioningState'), 'InProgress')

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
@mock.patch('azure.cli.command_modules.appservice.custom._get_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_request_body')
@mock.patch('azure.cli.command_modules.appservice.custom._build_onedeploy_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_status_url')
@mock.patch('azure.cli.command_modules.appservice.custom._get_ondeploy_headers')
def test_src_url_no_deployment_id(self, headers_mock, status_url_mock, build_url_mock_onedeploy,
body_mock, get_url_mock, send_raw_mock):
"""Test that --src-url falls back gracefully when no deployment ID is found"""
from azure.cli.command_modules.appservice.custom import _make_onedeploy_request

# Mock helper functions
body_mock.return_value = ('{"type": "zip"}', None)
build_url_mock_onedeploy.return_value = 'https://management.azure.com/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/sites/myapp/extensions/onedeploy'
status_url_mock.return_value = 'https://myapp.scm.azurewebsites.net/api/deployments/latest'
headers_mock.return_value = {'Content-Type': 'application/json'}

# Mock the ARM response without deployment ID
class MockResponse:
status_code = 200
headers = {'content-type': 'application/json'}
text = '{}'

def json(self):
return {
'properties': {'provisioningState': 'Succeeded'}
}

send_raw_mock.return_value = MockResponse()
get_url_mock.return_value = 'https://myapp.azurewebsites.net'

# Create params object
class Params:
src_url = 'https://example.com/myapp.zip'
src_path = None
is_async_deployment = None # Default
cmd = _get_test_cmd()
resource_group_name = 'test-rg'
webapp_name = 'test-app'
slot = None
timeout = None
is_linux_webapp = False
is_functionapp = False
enable_kudu_warmup = False

params = Params()

# Execute
result = _make_onedeploy_request(params)

# Assert result is immediate response (no polling)
self.assertEqual(result.get('provisioningState'), 'Succeeded')


@mock.patch('azure.cli.command_modules.appservice.custom._poll_deployment_runtime_status',
side_effect=RuntimeError("Simulated polling failure"))
@mock.patch('azure.cli.command_modules.appservice.custom._build_deploymentstatus_url',
return_value='https://management.azure.com/.../deploymentStatus/abc123')
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
@mock.patch('azure.cli.command_modules.appservice.custom._get_url',
return_value='https://test-app.azurewebsites.net')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_request_body',
return_value=('{"type": "zip"}', None))
@mock.patch('azure.cli.command_modules.appservice.custom._build_onedeploy_url',
return_value='https://management.azure.com/.../onedeploy')
@mock.patch('azure.cli.command_modules.appservice.custom._get_onedeploy_status_url',
return_value='https://myapp.scm.azurewebsites.net/api/deployments/latest')
@mock.patch('azure.cli.command_modules.appservice.custom._get_ondeploy_headers',
return_value={'Content-Type': 'application/json'})
def test_src_url_fallback_on_poll_exception(self, headers_mock, status_url_mock,
build_url_mock_onedeploy, body_mock,
get_url_mock, send_raw_mock,
build_status_url_mock, poll_mock):
"""Test that when _poll_deployment_runtime_status raises, the fallback returns the ARM response."""
from azure.cli.command_modules.appservice.custom import _make_onedeploy_request

class MockResponse:
status_code = 200
headers = {'content-type': 'application/json'}
text = '{"id": "/subs/sub/rg/rg/providers/Microsoft.Web/sites/app/extensions/onedeploy/abc123"}'

def json(self):
return {
'id': '/subs/sub/rg/rg/providers/Microsoft.Web/sites/app/extensions/onedeploy/abc123',
'properties': {'provisioningState': 'InProgress', 'deployer': 'ZipDeploy'}
}

send_raw_mock.return_value = MockResponse()

class Params:
src_url = 'https://example.com/myapp.zip'
src_path = None
is_async_deployment = None # sync mode
cmd = _get_test_cmd()
resource_group_name = 'test-rg'
webapp_name = 'test-app'
slot = None
timeout = None
is_linux_webapp = False
is_functionapp = False
enable_kudu_warmup = False

params = Params()
result = _make_onedeploy_request(params)

# Polling was attempted and raised
poll_mock.assert_called_once()
# Fallback: ARM response properties are returned instead of crashing
self.assertIsNotNone(result)
self.assertEqual(result.get('provisioningState'), 'InProgress')
self.assertEqual(result.get('deployer'), 'ZipDeploy')

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