diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_deployment_context_engine.py b/src/azure-cli/azure/cli/command_modules/appservice/_deployment_context_engine.py index e0e41bbbd27..b8dd3f6f4bd 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_deployment_context_engine.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_deployment_context_engine.py @@ -332,3 +332,87 @@ def raise_enriched_plan_error(*, resource_group_name=None, plan_name=None, message = format_enriched_plan_error_message(context) raise EnrichedDeploymentError(message) + + +def build_enriched_webapp_create_error_context(*, resource_group_name=None, webapp_name=None, + plan_name=None, location=None, sku=None, runtime=None, + status_code=None, error_message=None, last_known_step=None): + from ._deployment_failure_patterns import match_webapp_create_failure_pattern + + pattern = match_webapp_create_failure_pattern( + status_code=status_code, + error_message=error_message, + ) + + context = {} + if pattern: + context["errorCode"] = pattern["errorCode"] + context["stage"] = pattern["stage"] + context["suggestedFixes"] = pattern["suggestedFixes"] + else: + context["errorCode"] = f"HTTP_{status_code}" if status_code else "UnknownWebAppCreateError" + context["stage"] = "ResourceProvisioning" + context["suggestedFixes"] = [ + "Correct the property or value identified in Raw Error and retry 'az webapp create'", + "If the failing property is optional, remove its argument to use the App Service default", + "Run 'az webapp create --help' to verify supported arguments and values" + ] + + context["resourceGroup"] = resource_group_name or "Unknown" + context["webappName"] = webapp_name or "Unknown" + context["planName"] = plan_name or "Unknown" + context["region"] = location or "Unknown" + context["planSku"] = sku or "Unknown" + context["runtime"] = runtime or "Unknown" + + if last_known_step: + context["lastKnownStep"] = last_known_step + if error_message: + context["rawError"] = (error_message[:500] + "... [truncated]" + if len(error_message) > 500 else error_message) + + return context + + +def format_enriched_webapp_create_error_message(context): + lines = [ + "", + "=" * 72, + "WEB APP CREATION FAILED: Context-Enriched Diagnostics", + "=" * 72, + "", + f"Error Code : {context.get('errorCode', 'Unknown')}", + f"Stage : {context.get('stage', 'Unknown')}", + f"Web App Name: {context.get('webappName', 'Unknown')}", + f"Resource Grp: {context.get('resourceGroup', 'Unknown')}", + f"Plan Name : {context.get('planName', 'Unknown')}", + f"Region : {context.get('region', 'Unknown')}", + f"Plan SKU : {context.get('planSku', 'Unknown')}", + f"Runtime : {context.get('runtime', 'Unknown')}", + ] + if context.get("lastKnownStep"): + lines.append(f"Last Step : {context['lastKnownStep']}") + lines.append("") + + if context.get("rawError"): + lines.extend([f"Raw Error : {context['rawError']}", ""]) + + fixes = context.get("suggestedFixes", []) + if fixes: + lines.append("Suggested Fixes:") + lines.extend(f" - {fix}" for fix in fixes) + lines.append("") + + lines.extend([ + "-" * 72, + " Copy the full error output above and paste it into GitHub Copilot Chat", + " with the prompt: 'Why did my az webapp create fail and how do I fix it?'", + "-" * 72, + ]) + return "\n".join(lines) + + +def raise_enriched_webapp_create_error(**kwargs): + context = build_enriched_webapp_create_error_context(**kwargs) + logger.debug("Web app creation failure context: %s", context) + raise EnrichedDeploymentError(format_enriched_webapp_create_error_message(context)) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_deployment_failure_patterns.py b/src/azure-cli/azure/cli/command_modules/appservice/_deployment_failure_patterns.py index 4014aeab090..03366118a85 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_deployment_failure_patterns.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_deployment_failure_patterns.py @@ -183,8 +183,56 @@ _CONTROL_PLANE_PATTERN_INDEX = {p["errorCode"]: p for p in CONTROL_PLANE_FAILURE_PATTERNS} +WEBAPP_CREATE_FAILURE_PATTERNS = [ + { + "errorCode": "InvalidLinuxRuntime", + "stage": "RequestValidation", + "suggestedFixes": [ + "List supported Linux runtimes: 'az webapp list-runtimes --os-type linux -o table'", + "Set --runtime to a supported STACK:VERSION value from that list" + ] + }, + { + "errorCode": "InvalidMinTlsCipherSuite", + "stage": "RequestValidation", + "suggestedFixes": [ + "Set --min-tls-cipher-suite to one of the acceptable values listed in Raw Error", + "Or remove --min-tls-cipher-suite to use the App Service default cipher suite" + ] + }, + { + "errorCode": "SiteNameUnavailable", + "stage": "ResourceProvisioning", + "suggestedFixes": [ + "Choose a globally unique web app name and retry 'az webapp create'", + "Check name availability with a different --name value" + ] + }, + { + "errorCode": "ServerFarmNotFound", + "stage": "ResourceProvisioning", + "suggestedFixes": [ + "Verify the App Service plan still exists: 'az appservice plan show -g -n '", + "Pass the full resource ID with --plan when the plan is in another resource group" + ] + }, + { + "errorCode": "LinuxWorkersUnavailable", + "stage": "ResourceProvisioning", + "suggestedFixes": [ + "Choose a region with Linux capacity: 'az appservice list-locations --linux-workers-enabled'", + "Create a Linux plan in a supported region and retry 'az webapp create'" + ] + }, +] + +_WEBAPP_CREATE_PATTERN_INDEX = {p["errorCode"]: p for p in WEBAPP_CREATE_FAILURE_PATTERNS} + + def get_failure_pattern(error_code): - return _PATTERN_INDEX.get(error_code) or _CONTROL_PLANE_PATTERN_INDEX.get(error_code) + return (_PATTERN_INDEX.get(error_code) or + _CONTROL_PLANE_PATTERN_INDEX.get(error_code) or + _WEBAPP_CREATE_PATTERN_INDEX.get(error_code)) def match_failure_pattern(status_code=None, error_message=None): # pylint: disable=too-many-return-statements,too-many-branches @@ -258,3 +306,30 @@ def match_control_plane_failure_pattern(status_code=None, error_message=None): # pattern above is left unclassified (returns None) so the caller produces a # generic HTTP_400 context rather than a potentially wrong SKU diagnosis. return None + + +def match_webapp_create_failure_pattern(status_code=None, error_message=None): # pylint: disable=too-many-return-statements + """Map a web app create ARM failure to a known pattern, if possible.""" + error_lower = (error_message or "").lower() + + if "linux runtime" in error_lower and "not supported" in error_lower: + return get_failure_pattern("InvalidLinuxRuntime") + if "mintlsciphersuite" in error_lower and "invalid" in error_lower: + return get_failure_pattern("InvalidMinTlsCipherSuite") + if ("hostnameconflict" in error_lower or "sitealreadyexists" in error_lower or + ("name" in error_lower and ("already exists" in error_lower or "not available" in error_lower))): + return get_failure_pattern("SiteNameUnavailable") + mentions_plan = any(term in error_lower for term in ("serverfarm", "server farm", "app service plan")) + plan_not_found = any(term in error_lower for term in ("not found", "could not be found", "cannot find")) + if mentions_plan and plan_not_found: + return get_failure_pattern("ServerFarmNotFound") + if "linux" in error_lower and "worker" in error_lower and \ + ("not available" in error_lower or "unavailable" in error_lower or "capacity" in error_lower): + return get_failure_pattern("LinuxWorkersUnavailable") + + pattern = match_control_plane_failure_pattern(status_code=status_code, error_message=error_message) + if status_code == 409 and pattern and pattern["errorCode"] == "MissingSubscriptionRegistration" and \ + "missingsubscriptionregistration" not in error_lower and \ + "not registered to use namespace" not in error_lower: + return None + return pattern diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 775fe497149..674321d59aa 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -360,6 +360,10 @@ def load_arguments(self, _): help="The minimum version of TLS required for SSL requests, e.g., '1.0', '1.1', '1.2'") c.argument('min_tls_cipher_suite', options_list=['--min-tls-cipher-suite'], help="The minimum TLS Cipher Suite required for requests, e.g., 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384'") + c.argument('enriched_errors', options_list=['--enriched-errors'], + help='If true, Linux web app creation failures will show context-enriched diagnostics with ' + 'error codes, suggested fixes, and Copilot prompts. This flag only applies to Linux apps.', + arg_type=get_three_state_flag()) c.ignore('language') c.ignore('using_webapp_up') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 0b50be163bd..fa75e8b236b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -62,7 +62,7 @@ from ._appservice_utils import MSI_LOCAL_ID from ._deployment_context_engine import ( raise_enriched_deployment_error, EnrichedDeploymentError, - raise_enriched_plan_error, extract_status_code_from_message + raise_enriched_plan_error, raise_enriched_webapp_create_error, extract_status_code_from_message ) from .utils import (_normalize_sku, get_sku_tier, @@ -132,7 +132,8 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi role='Contributor', scope=None, vnet=None, subnet=None, https_only=False, public_network_access=None, acr_use_identity=False, acr_identity=None, basic_auth="", auto_generated_domain_name_label_scope=None, end_to_end_encryption_enabled=None, - min_tls_version=None, min_tls_cipher_suite=None, site_scoped_certs=None): + min_tls_version=None, min_tls_cipher_suite=None, site_scoped_certs=None, + enriched_errors=False): from azure.mgmt.web.models import Site, OutboundVnetRouting from azure.core.exceptions import ResourceNotFoundError as _ResourceNotFoundError SiteConfig, SkuDescription, NameValuePair = cmd.get_models( @@ -299,8 +300,20 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi elif runtime: match = helper.resolve(runtime, is_linux) if not match: - raise ValidationError("Linux Runtime '{}' is not supported." - "Run 'az webapp list-runtimes --os-type linux' to cross check".format(runtime)) + error_message = ("Linux Runtime '{}' is not supported. " + "Run 'az webapp list-runtimes --os-type linux' to cross check".format(runtime)) + if enriched_errors: + raise_enriched_webapp_create_error( + resource_group_name=resource_group_name, + webapp_name=name, + plan_name=getattr(plan_info, 'name', None) or plan, + location=location, + sku=getattr(getattr(plan_info, 'sku', None), 'name', None), + runtime=runtime, + error_message=error_message, + last_known_step="Linux runtime validation", + ) + raise ValidationError(error_message) helper.get_site_config_setter(match, linux=is_linux)(cmd=cmd, stack=match, site_config=site_config) elif container_image_name: site_config.linux_fx_version = _format_fx_version(container_image_name) @@ -359,8 +372,26 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi value='https://{}.scm.azurewebsites.net/detectors' .format(name))) - poller = client.web_apps.begin_create_or_update(resource_group_name, name, webapp_def) - webapp = LongRunningOperation(cmd.cli_ctx)(poller) + try: + poller = client.web_apps.begin_create_or_update(resource_group_name, name, webapp_def) + webapp = LongRunningOperation(cmd.cli_ctx)(poller) + except EnrichedDeploymentError: + raise + except Exception as ex: # pylint: disable=broad-except + if not (enriched_errors and is_linux): + raise + error_message, status_code = _get_enriched_error_details(ex) + raise_enriched_webapp_create_error( + resource_group_name=resource_group_name, + webapp_name=name, + plan_name=getattr(plan_info, 'name', None) or plan, + location=location, + sku=getattr(getattr(plan_info, 'sku', None), 'name', None), + runtime=site_config.linux_fx_version, + status_code=status_code, + error_message=error_message, + last_known_step="Web app create (control-plane request)", + ) if current_stack: _update_webapp_current_stack_property_if_needed(cmd, resource_group_name, name, current_stack) @@ -5028,7 +5059,7 @@ def is_async_response(poller, timeout_seconds=30): return status_code == 202 -def _raise_enriched_plan_create_error(ex, resource_group_name, name, location, sku): +def _get_enriched_error_details(ex): message_parts = [] top_message = getattr(ex, 'message', None) if top_message: @@ -5051,6 +5082,11 @@ def _raise_enriched_plan_create_error(ex, resource_group_name, name, location, s status_code = getattr(response, 'status_code', None) if status_code is None: status_code = extract_status_code_from_message(error_message) + return error_message, status_code + + +def _raise_enriched_plan_create_error(ex, resource_group_name, name, location, sku): + error_message, status_code = _get_enriched_error_details(ex) raise_enriched_plan_error( resource_group_name=resource_group_name, plan_name=name, diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_deployment_context_engine.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_deployment_context_engine.py index aade1c5c139..444eeb1d700 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_deployment_context_engine.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_deployment_context_engine.py @@ -15,9 +15,11 @@ from azure.cli.command_modules.appservice._deployment_failure_patterns import ( DEPLOYMENT_FAILURE_PATTERNS, CONTROL_PLANE_FAILURE_PATTERNS, + WEBAPP_CREATE_FAILURE_PATTERNS, get_failure_pattern, match_failure_pattern, match_control_plane_failure_pattern, + match_webapp_create_failure_pattern, ) from azure.cli.command_modules.appservice._deployment_context_engine import ( build_enriched_error_context, @@ -29,6 +31,9 @@ build_enriched_plan_error_context, format_enriched_plan_error_message, raise_enriched_plan_error, + build_enriched_webapp_create_error_context, + format_enriched_webapp_create_error_message, + raise_enriched_webapp_create_error, ) @@ -633,5 +638,129 @@ def test_raise_enriched_plan_error(self): self.assertIn("Pick a region where App Service plans", error_msg) +# --------------------------------------------------------------------------- +# Tests for webapp-create enrichment (match / build / format / raise) +# --------------------------------------------------------------------------- +class TestWebappCreateEnrichment(unittest.TestCase): + """Tests for the Linux web app creation enrichment engine.""" + + def test_all_patterns_have_required_keys(self): + required_keys = {"errorCode", "stage", "suggestedFixes"} + for pattern in WEBAPP_CREATE_FAILURE_PATTERNS: + with self.subTest(errorCode=pattern["errorCode"]): + self.assertTrue(required_keys.issubset(pattern.keys())) + self.assertGreater(len(pattern["suggestedFixes"]), 0) + + def test_match_linux_workers_unavailable(self): + pattern = match_webapp_create_failure_pattern( + status_code=400, + error_message="Linux workers are not available in this region due to capacity constraints", + ) + self.assertEqual(pattern["errorCode"], "LinuxWorkersUnavailable") + + def test_match_invalid_linux_runtime(self): + pattern = match_webapp_create_failure_pattern( + error_message="Linux Runtime 'PYTHON|99.99' is not supported.", + ) + self.assertEqual(pattern["errorCode"], "InvalidLinuxRuntime") + self.assertEqual(pattern["stage"], "RequestValidation") + + def test_match_invalid_min_tls_cipher_suite(self): + pattern = match_webapp_create_failure_pattern( + status_code=400, + error_message="The parameter 'MinTlsCipherSuite' has an invalid value. " + "Acceptable values are: TLS_AES_256_GCM_SHA384", + ) + self.assertEqual(pattern["errorCode"], "InvalidMinTlsCipherSuite") + self.assertEqual(pattern["stage"], "RequestValidation") + + def test_match_server_farm_not_found_live_service_wording(self): + pattern = match_webapp_create_failure_pattern( + status_code=404, + error_message="Cannot find serverFarm with name plan-does-not-exist.", + ) + self.assertEqual(pattern["errorCode"], "ServerFarmNotFound") + + def test_match_known_control_plane_failure(self): + pattern = match_webapp_create_failure_pattern( + status_code=403, + error_message="AuthorizationFailed: The client does not have authorization", + ) + self.assertEqual(pattern["errorCode"], "AuthorizationFailed") + + def test_generic_conflict_is_not_misclassified_as_registration_failure(self): + pattern = match_webapp_create_failure_pattern(status_code=409, error_message="Conflict") + self.assertIsNone(pattern) + + def test_build_context_with_known_pattern(self): + context = build_enriched_webapp_create_error_context( + resource_group_name="test-rg", + webapp_name="test-app", + plan_name="test-plan", + location="westus2", + sku="B1", + runtime="PYTHON|3.11", + status_code=400, + error_message="Linux workers are unavailable due to capacity", + last_known_step="Web app create (control-plane request)", + ) + self.assertEqual(context["errorCode"], "LinuxWorkersUnavailable") + self.assertEqual(context["webappName"], "test-app") + self.assertEqual(context["planName"], "test-plan") + self.assertEqual(context["runtime"], "PYTHON|3.11") + self.assertIn("rawError", context) + + def test_invalid_min_tls_cipher_context_has_actionable_fixes(self): + context = build_enriched_webapp_create_error_context( + status_code=400, + error_message="The parameter 'MinTlsCipherSuite' has an invalid value. " + "Acceptable values are: TLS_AES_256_GCM_SHA384", + ) + self.assertEqual(context["errorCode"], "InvalidMinTlsCipherSuite") + self.assertIn("--min-tls-cipher-suite", context["suggestedFixes"][0]) + self.assertIn("acceptable values listed in Raw Error", context["suggestedFixes"][0]) + self.assertIn("remove --min-tls-cipher-suite", context["suggestedFixes"][1]) + + def test_unknown_error_has_property_focused_fixes(self): + context = build_enriched_webapp_create_error_context( + status_code=400, + error_message="An unknown property has an invalid value", + ) + self.assertEqual(context["errorCode"], "HTTP_400") + self.assertIn("property or value identified in Raw Error", context["suggestedFixes"][0]) + self.assertNotIn("globally unique", " ".join(context["suggestedFixes"])) + + def test_format_message_contains_create_details(self): + context = build_enriched_webapp_create_error_context( + resource_group_name="test-rg", + webapp_name="test-app", + plan_name="test-plan", + location="westus2", + sku="B1", + runtime="PYTHON|3.11", + status_code=403, + error_message="AuthorizationFailed: not authorized", + ) + message = format_enriched_webapp_create_error_message(context) + self.assertIn("WEB APP CREATION FAILED", message) + self.assertIn("AuthorizationFailed", message) + self.assertIn("Web App Name: test-app", message) + self.assertIn("Plan Name : test-plan", message) + self.assertIn("Runtime : PYTHON|3.11", message) + self.assertNotIn("Deploy Type", message) + + def test_raise_enriched_webapp_create_error(self): + with self.assertRaises(EnrichedDeploymentError) as cm: + raise_enriched_webapp_create_error( + resource_group_name="test-rg", + webapp_name="test-app", + plan_name="test-plan", + status_code=409, + error_message="HostNameConflict: site name already exists", + ) + self.assertIn("WEB APP CREATION FAILED", str(cm.exception)) + self.assertIn("SiteNameUnavailable", str(cm.exception)) + + if __name__ == '__main__': unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 7ff3befa282..085f2e86119 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -16,7 +16,8 @@ MutuallyExclusiveArgumentError, ArgumentUsageError, AzureResponseError, - ResourceNotFoundError) + ResourceNotFoundError, + ValidationError) from azure.cli.command_modules.appservice.custom import (set_deployment_user, update_git_token, add_hostname, update_site_configs, @@ -42,6 +43,7 @@ show_startup_log, troubleshoot_status, create_webapp) +from azure.cli.command_modules.appservice._deployment_context_engine import EnrichedDeploymentError # pylint: disable=line-too-long from azure.cli.core.profiles import ResourceType @@ -473,6 +475,110 @@ def test_linux_webapp_create_no_runtime_raises_error(self, get_site_avail_mock, self.assertIn('--runtime', str(context.exception)) self.assertIn('--os-type linux', str(context.exception)) + @staticmethod + def _configure_webapp_create_failure(get_site_avail_mock, stack_helper_mock, web_client_mock, + is_linux, error): + cmd_mock = _get_test_cmd() + SiteConfig, SkuDescription, NameValuePair = cmd_mock.get_models( + 'SiteConfig', 'SkuDescription', 'NameValuePair') + cmd_mock.get_models = mock.MagicMock(return_value=(SiteConfig, SkuDescription, NameValuePair)) + + plan_info = mock.MagicMock() + plan_info.name = 'test-plan' + plan_info.reserved = is_linux + plan_info.is_xenon = False + plan_info.location = 'westus2' + plan_info.id = '/subscriptions/sub/resourceGroups/test-rg/providers/Microsoft.Web/serverfarms/test-plan' + plan_info.sku = SkuDescription(name='B1') + web_client_mock.return_value.app_service_plans.get.return_value = plan_info + web_client_mock.return_value.web_apps.begin_create_or_update.side_effect = error + + name_validation = mock.MagicMock() + name_validation.name_available = True + get_site_avail_mock.return_value = name_validation + stack_helper_mock.return_value.get_default_version.return_value = '20.0' + return cmd_mock + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_availability', autospec=True) + def test_linux_webapp_create_enriches_control_plane_failure(self, get_site_avail_mock, + stack_helper_mock, web_client_mock): + error = RuntimeError('Status Code: 400 Linux workers are unavailable due to capacity') + cmd_mock = self._configure_webapp_create_failure( + get_site_avail_mock, stack_helper_mock, web_client_mock, True, error) + + with self.assertRaises(EnrichedDeploymentError) as context: + create_webapp(cmd_mock, 'test-rg', 'test-app', 'test-plan', + container_image_name='nginx:latest', enriched_errors=True) + + self.assertIn('WEB APP CREATION FAILED', str(context.exception)) + self.assertIn('LinuxWorkersUnavailable', str(context.exception)) + self.assertIn('Runtime : DOCKER|nginx:latest', str(context.exception)) + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_availability', autospec=True) + def test_linux_webapp_create_enriches_invalid_runtime(self, get_site_avail_mock, + stack_helper_mock, web_client_mock): + cmd_mock = self._configure_webapp_create_failure( + get_site_avail_mock, stack_helper_mock, web_client_mock, True, None) + stack_helper_mock.remove_delimiters.return_value = 'PYTHON|99.99' + stack_helper_mock.return_value.resolve.return_value = None + + with self.assertRaises(EnrichedDeploymentError) as context: + create_webapp(cmd_mock, 'test-rg', 'test-app', 'test-plan', + runtime='PYTHON:99.99', enriched_errors=True) + + self.assertIn('InvalidLinuxRuntime', str(context.exception)) + self.assertIn('Runtime : PYTHON|99.99', str(context.exception)) + self.assertIn('az webapp list-runtimes --os-type linux -o table', str(context.exception)) + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_availability', autospec=True) + def test_linux_webapp_create_preserves_invalid_runtime_when_enrichment_disabled( + self, get_site_avail_mock, stack_helper_mock, web_client_mock): + cmd_mock = self._configure_webapp_create_failure( + get_site_avail_mock, stack_helper_mock, web_client_mock, True, None) + stack_helper_mock.remove_delimiters.return_value = 'PYTHON|99.99' + stack_helper_mock.return_value.resolve.return_value = None + + with self.assertRaises(ValidationError) as context: + create_webapp(cmd_mock, 'test-rg', 'test-app', 'test-plan', + runtime='PYTHON:99.99', enriched_errors=False) + + self.assertIn("Linux Runtime 'PYTHON|99.99' is not supported", str(context.exception)) + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_availability', autospec=True) + def test_linux_webapp_create_preserves_failure_when_enrichment_disabled(self, get_site_avail_mock, + stack_helper_mock, web_client_mock): + error = RuntimeError('Status Code: 400 Linux workers are unavailable due to capacity') + cmd_mock = self._configure_webapp_create_failure( + get_site_avail_mock, stack_helper_mock, web_client_mock, True, error) + + with self.assertRaises(RuntimeError) as context: + create_webapp(cmd_mock, 'test-rg', 'test-app', 'test-plan', + container_image_name='nginx:latest', enriched_errors=False) + + self.assertIs(context.exception, error) + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._StackRuntimeHelper', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.get_site_availability', autospec=True) + def test_windows_webapp_create_preserves_failure_when_enrichment_enabled(self, get_site_avail_mock, + stack_helper_mock, web_client_mock): + error = RuntimeError('Status Code: 400 bad request') + cmd_mock = self._configure_webapp_create_failure( + get_site_avail_mock, stack_helper_mock, web_client_mock, False, error) + + with self.assertRaises(RuntimeError) as context: + create_webapp(cmd_mock, 'test-rg', 'test-app', 'test-plan', enriched_errors=True) + + self.assertIs(context.exception, error) + @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', 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)