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
2 changes: 2 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 @@ -1776,6 +1776,8 @@
examples:
- name: Create a Managed Certificate for cname.mycustomdomain.com.
text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname cname.mycustomdomain.com
- name: Create a Managed Certificate and wait for it to complete (up to 10 minutes).
text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname cname.mycustomdomain.com --wait
"""

helps['webapp config storage-account'] = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ def load_arguments(self, _):
c.argument('hostname', help='The custom domain name')
c.argument('name', options_list=['--name', '-n'], help='Name of the web app.')
c.argument('resource-group', options_list=['--resource-group', '-g'], help='Name of resource group.')
c.argument('wait', options_list=['--wait'], action='store_true', default=False,
help='Wait up to 10 minutes for the certificate to be created. '
'Returns an error if creation times out instead of silently returning.')
Comment on lines +524 to +526
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait is being added inside the for scope in ['webapp', 'functionapp'] loop, so --wait will also become available on az functionapp config ssl create (and will map into create_managed_ssl_cert). If this PR is intended to be webapp-only, scope this argument to webapp instead; if it’s intended to support functionapps too, please update functionapp help/examples (and ideally add coverage) to reflect the new flag.

Suggested change
c.argument('wait', options_list=['--wait'], action='store_true', default=False,
help='Wait up to 10 minutes for the certificate to be created. '
'Returns an error if creation times out instead of silently returning.')
if scope == 'webapp':
c.argument('wait', options_list=['--wait'], action='store_true', default=False,
help='Wait up to 10 minutes for the certificate to be created. '
'Returns an error if creation times out instead of silently returning.')

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --wait help text ends with “instead of silently returning.” but doesn’t say what’s returned (it’s None). Consider clarifying this to “silently returning None/no result” so automation users understand the default behavior precisely.

Suggested change
'Returns an error if creation times out instead of silently returning.')
'Returns an error if creation times out instead of silently returning None (no result).')

Copilot uses AI. Check for mistakes.
with self.argument_context(scope + ' config hostname') as c:
c.argument('hostname', completer=get_hostname_completion_list,
help="hostname assigned to the site, such as custom domains", id_part='child_name_1')
Expand Down
10 changes: 8 additions & 2 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -5960,7 +5960,7 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n
certificate_envelope=kv_cert_def)


def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, certificate_name=None):
def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, certificate_name=None, wait=False):
Certificate = cmd.get_models('Certificate')
hostname = hostname.lower()
client = web_client_factory(cmd.cli_ctx)
Expand Down Expand Up @@ -5997,7 +5997,8 @@ def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None,
poll_url = ex.response.headers['Location'] if 'Location' in ex.response.headers else None
if ex.response.status_code == 202 and poll_url:
r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url)
poll_timeout = time.time() + 60 * 2 # 2 minute timeout
poll_timeout_minutes = 10 if wait else 2
poll_timeout = time.time() + 60 * poll_timeout_minutes

while r.status_code != 200 and time.time() < poll_timeout:
time.sleep(5)
Expand All @@ -6008,6 +6009,11 @@ def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None,
return r.json()
except ValueError:
return r.text
if wait:
raise CLIError("Managed Certificate creation for '{}' timed out after {} minutes. "
"Check status with 'az webapp config ssl show -g {} "
"--certificate-name {}'.".format(hostname, poll_timeout_minutes,
resource_group_name, certificate_name))
logger.warning("Managed Certificate creation in progress. Please use the command "
"'az webapp config ssl show -g %s --certificate-name %s' "
" to view your certificate once it is created", resource_group_name, certificate_name)
Comment on lines +6012 to 6019
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This timeout error hardcodes az webapp config ssl show ..., but create_managed_ssl_cert is also used by functionapp config ssl create (see commands.py). If --wait is exposed for functionapps (currently it is via _params.py), the guidance/error message should be accurate for both command groups (e.g., mention both webapp and functionapp, or avoid hardcoding the group).

Suggested change
if wait:
raise CLIError("Managed Certificate creation for '{}' timed out after {} minutes. "
"Check status with 'az webapp config ssl show -g {} "
"--certificate-name {}'.".format(hostname, poll_timeout_minutes,
resource_group_name, certificate_name))
logger.warning("Managed Certificate creation in progress. Please use the command "
"'az webapp config ssl show -g %s --certificate-name %s' "
" to view your certificate once it is created", resource_group_name, certificate_name)
app_type = 'functionapp' if is_functionapp(cmd, resource_group_name, name) else 'webapp'
if wait:
raise CLIError("Managed Certificate creation for '{}' timed out after {} minutes. "
"Check status with 'az {} config ssl show -g {} "
"--certificate-name {}'.".format(hostname, poll_timeout_minutes,
app_type, resource_group_name, certificate_name))
logger.warning("Managed Certificate creation in progress. Please use the command "
"'az %s config ssl show -g %s --certificate-name %s' "
" to view your certificate once it is created", app_type, resource_group_name,
certificate_name)

Copilot uses AI. Check for mistakes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,94 @@ def test_create_managed_ssl_cert(self, generic_site_op_mock, client_factory_mock
certificate_envelope=cert_def)


@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True)
def test_create_managed_ssl_cert_wait_timeout_raises_error(self, generic_site_op_mock, client_factory_mock,
verify_binding_mock, send_raw_request_mock):
"""Test that --wait raises CLIError on timeout instead of returning None."""
webapp_name = 'someWebAppName'
rg_name = 'someRgName'
farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1'
host_name = 'www.contoso.com'

client = mock.MagicMock()
client_factory_mock.return_value = client
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cli_ctx_mock.data = {'subscription_id': 'sub1'}
cmd_mock.cli_ctx = cli_ctx_mock
Site, Certificate = cmd_mock.get_models('Site', 'Certificate')
site = Site(name=webapp_name, location='westeurope')
site.server_farm_id = farm_id
generic_site_op_mock.return_value = site
verify_binding_mock.return_value = True

# Simulate 202 with Location header
ex_response = mock.MagicMock()
ex_response.status_code = 202
ex_response.headers = {'Location': 'https://polling-url'}
api_exception = Exception('accepted')
api_exception.response = ex_response
client.certificates.create_or_update.side_effect = api_exception

# Polling always returns 202 (never completes)
poll_response = mock.MagicMock()
poll_response.status_code = 202
send_raw_request_mock.return_value = poll_response

# With wait=True and mocked time to simulate immediate timeout
with mock.patch('azure.cli.command_modules.appservice.custom.time') as time_mock:
time_mock.time.side_effect = [0, 999999] # Start, then past timeout
time_mock.sleep = mock.MagicMock()
with self.assertRaises(CLIError):
create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None, wait=True)

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True)
def test_create_managed_ssl_cert_no_wait_returns_none(self, generic_site_op_mock, client_factory_mock,
verify_binding_mock, send_raw_request_mock):
"""Test that without --wait, timeout returns None with a warning (default behavior)."""
webapp_name = 'someWebAppName'
rg_name = 'someRgName'
farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1'
host_name = 'www.contoso.com'

client = mock.MagicMock()
client_factory_mock.return_value = client
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cli_ctx_mock.data = {'subscription_id': 'sub1'}
cmd_mock.cli_ctx = cli_ctx_mock
Site, Certificate = cmd_mock.get_models('Site', 'Certificate')
site = Site(name=webapp_name, location='westeurope')
site.server_farm_id = farm_id
generic_site_op_mock.return_value = site
verify_binding_mock.return_value = True

# Simulate 202 with Location header
ex_response = mock.MagicMock()
ex_response.status_code = 202
ex_response.headers = {'Location': 'https://polling-url'}
api_exception = Exception('accepted')
api_exception.response = ex_response
client.certificates.create_or_update.side_effect = api_exception

# Polling always returns 202
poll_response = mock.MagicMock()
poll_response.status_code = 202
send_raw_request_mock.return_value = poll_response

# Without wait (default), should return None, not raise
with mock.patch('azure.cli.command_modules.appservice.custom.time') as time_mock:
time_mock.time.side_effect = [0, 999999]
time_mock.sleep = mock.MagicMock()
result = create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None, wait=False)
self.assertIsNone(result)

def test_update_app_settings_error_handling_no_parameters(self):
"""Test that MutuallyExclusiveArgumentError is raised when neither settings nor slot_settings are provided."""
cmd_mock = _get_test_cmd()
Expand Down
Loading