Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Original file line number Diff line number Diff line change
Expand Up @@ -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 <rg> -n <plan>'",
"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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
50 changes: 43 additions & 7 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down
Loading
Loading