Skip to content
Draft
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
43 changes: 32 additions & 11 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9652,21 +9652,40 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url,
# Indicates whether the status has been non empty in previous calls
has_response = False
has_partial_success = False
# Indicates Kudu is restarting after package deployment (transient state for flex consumption apps)
kudu_restart_in_progress = False
# Tracks whether the Kudu-restart 404 warning has already been emitted once
kudu_restart_404_warned = False
# Default return value; overwritten on each successful JSON response
res_dict = {}
while num_trials < total_trials:
time.sleep(1)
response = requests.get(deployment_status_url, headers=headers,
verify=not should_disable_connection_verify())
try:
if response.status_code == 202 and not has_partial_success:
has_partial_success = True
if response.status_code == 404 and has_partial_success:
break
if (response.status_code == 404 or response.json().get('status') is None) and has_response:
raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.")
if (response.status_code != 404 and response.json().get('status') is not None) and not has_response:
has_response = True

res_dict = response.json()
if response.status_code == 404:
if has_partial_success and not kudu_restart_in_progress:
break
elif kudu_restart_in_progress:
# Kudu is restarting after package deployment — continue polling until it comes back
if not kudu_restart_404_warned:
kudu_restart_404_warned = True
logger.warning("Deployment status endpoint returned 404. "
"Kudu may be restarting after package deployment. Retrying...")
else:
logger.info("Deployment status endpoint still returning 404 during Kudu restart. Retrying...")
elif has_response:
raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.")
else:
res_json = response.json()
res_status = res_json.get('status')
if res_status is None and has_response:
raise CLIError("Failed to retrieve deployment status. Please try again in a few minutes.")
if res_status is not None and not has_response:
has_response = True
res_dict = res_json
except json.decoder.JSONDecodeError:
logger.warning("Deployment status endpoint %s returns malformed data. Retrying...", deployment_status_url)
res_dict = {}
Expand All @@ -9685,12 +9704,14 @@ def _check_zip_deployment_status_flex(cmd, rg_name, name, deployment_status_url,
if status == 5:
raise CLIError("Deployment was cancelled and another deployment is in progress.")
if status == 6:
raise CLIError("Deployment was partially successful. These are the deployment logs:\n{}".format(
json.dumps(show_deployment_log(cmd, rg_name, name))))
if not kudu_restart_in_progress:
kudu_restart_in_progress = True
logger.warning("Deployment is partially complete. Kudu may be restarting after package "
"deployment. Continuing to poll for completion...")
if 'progress' in res_dict:
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 and not has_partial_success:
if res_dict.get('status', 0) != 4 and not has_partial_success and not kudu_restart_in_progress:
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 @@ -12,6 +12,7 @@
enable_zip_deploy_functionapp,
enable_zip_deploy,
enable_zip_deploy_flex,
_check_zip_deployment_status_flex,
add_remote_build_app_settings,
remove_remote_build_app_settings,
config_source_control,
Expand Down Expand Up @@ -681,3 +682,99 @@ def test_config_source_control(self,

# assert
self.assertEqual(response.git_hub_action_configuration.container_configuration.password, None)


class TestCheckZipDeploymentStatusFlex(unittest.TestCase):
"""Tests for _check_zip_deployment_status_flex handling of Kudu restart (status 6) scenario."""

def _make_response(self, status_code, json_body=None):
"""Create a mock HTTP response."""
resp = mock.MagicMock()
resp.status_code = status_code
if json_body is not None:
resp.json.return_value = json_body
else:
resp.json.side_effect = Exception("No JSON body")
return resp

@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={})
@mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('requests.get')
def test_status_6_then_status_4_succeeds(self, requests_get_mock, sleep_mock,
should_disable_mock, get_headers_mock):
"""Status 6 (Kudu restart) followed by status 4 (success) should not raise an error."""
cmd_mock = _get_test_cmd()

# Simulate: first poll returns status 6 (partial success / Kudu restarting),
# second poll returns status 4 (success)
requests_get_mock.side_effect = [
self._make_response(200, {'status': 6, 'progress': ''}),
self._make_response(200, {'status': 4, 'complete': True}),
]

result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name',
'https://mock-scm/api/deployments/latest',
timeout=None)
self.assertEqual(result.get('status'), 4)

@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={})
@mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('requests.get')
def test_status_6_then_404_then_status_4_succeeds(self, requests_get_mock, sleep_mock,
should_disable_mock, get_headers_mock):
"""Status 6 followed by 404 (Kudu restarting) followed by status 4 should succeed."""
cmd_mock = _get_test_cmd()

requests_get_mock.side_effect = [
self._make_response(200, {'status': 6, 'progress': ''}),
self._make_response(404),
self._make_response(200, {'status': 4, 'complete': True}),
]

result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name',
'https://mock-scm/api/deployments/latest',
timeout=None)
self.assertEqual(result.get('status'), 4)

@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={})
@mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('requests.get')
def test_status_6_multiple_404_then_status_4_succeeds(self, requests_get_mock, sleep_mock,
should_disable_mock, get_headers_mock):
"""Multiple 404 responses during Kudu restart should continue polling until success."""
cmd_mock = _get_test_cmd()

requests_get_mock.side_effect = [
self._make_response(200, {'status': 6, 'progress': ''}),
self._make_response(404),
self._make_response(404),
self._make_response(200, {'status': 4, 'complete': True}),
]

result = _check_zip_deployment_status_flex(cmd_mock, 'rg', 'name',
'https://mock-scm/api/deployments/latest',
timeout=None)
self.assertEqual(result.get('status'), 4)

@mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex', return_value={})
@mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
@mock.patch('time.sleep', return_value=None)
@mock.patch('requests.get')
def test_status_3_still_raises_error(self, requests_get_mock, sleep_mock,
should_disable_mock, get_headers_mock):
"""Status 3 (deployment failed) should still raise an error."""
cmd_mock = _get_test_cmd()

with mock.patch('azure.cli.command_modules.appservice.custom.show_deployment_log', return_value=[]):
requests_get_mock.side_effect = [
self._make_response(200, {'status': 3}),
]

with self.assertRaises(CLIError) as cm:
_check_zip_deployment_status_flex(cmd_mock, 'rg', 'name',
'https://mock-scm/api/deployments/latest',
timeout=None)
self.assertIn("Zip deployment failed", str(cm.exception))
Loading