Skip to content

Commit 3525f0c

Browse files
Refactor quickstart command to enhance error handling and improve deployment operation summaries
1 parent b6bb095 commit 3525f0c

1 file changed

Lines changed: 180 additions & 101 deletions

File tree

src/site/azext_site/aaz/latest/site/_quickstart.py

Lines changed: 180 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,91 @@ def _resolve_template_path() -> Path:
3131
return azext_root / "templates" / "infra" / "main.json"
3232

3333

34+
def _load_template_configuration_children(template_path: Path, config_id: str | None) -> list[dict]:
35+
"""Load child resources under Microsoft.Edge/Configurations from the bundled ARM template.
36+
37+
Best-effort only: this uses template defaults (no ARM reads).
38+
"""
39+
try:
40+
def _resolve_template_value(params: dict, value):
41+
"""Resolve a template value like "[parameters('x')]" to that parameter's defaultValue (best-effort)."""
42+
if not isinstance(value, str):
43+
return value
44+
45+
text = value.strip()
46+
prefix = "[parameters('"
47+
suffix = "')]"
48+
if text.startswith(prefix) and text.endswith(suffix):
49+
param_name = text[len(prefix):-len(suffix)]
50+
param_def = params.get(param_name)
51+
if isinstance(param_def, dict) and "defaultValue" in param_def:
52+
return param_def.get("defaultValue")
53+
return value
54+
55+
raw = template_path.read_text(encoding="utf-8")
56+
data = json.loads(raw)
57+
params = data.get("parameters") if isinstance(data, dict) else None
58+
params = params if isinstance(params, dict) else {}
59+
60+
resources = data.get("resources") if isinstance(data, dict) else None
61+
resources = resources if isinstance(resources, list) else []
62+
63+
config_resource = None
64+
for res in resources:
65+
if not isinstance(res, dict):
66+
continue
67+
if (res.get("type") or "").lower() == "microsoft.edge/configurations":
68+
config_resource = res
69+
break
70+
71+
if not isinstance(config_resource, dict):
72+
return []
73+
74+
child_resources = config_resource.get("resources")
75+
child_resources = child_resources if isinstance(child_resources, list) else []
76+
77+
children: list[dict] = []
78+
for child in child_resources:
79+
if not isinstance(child, dict):
80+
continue
81+
82+
child_type = child.get("type")
83+
if not isinstance(child_type, str) or not child_type:
84+
continue
85+
86+
child_name = _resolve_template_value(params, child.get("name"))
87+
if not isinstance(child_name, str) or not child_name:
88+
continue
89+
90+
child_kind = _resolve_template_value(params, child.get("kind"))
91+
if not isinstance(child_kind, str):
92+
child_kind = None
93+
94+
child_properties = _resolve_template_value(params, child.get("properties"))
95+
if not isinstance(child_properties, dict):
96+
child_properties = {}
97+
98+
child_id = None
99+
if config_id:
100+
child_id = f"{config_id}/{child_type}/{child_name}"
101+
102+
payload = {
103+
"id": child_id,
104+
"type": child_type,
105+
"name": child_name,
106+
"properties": child_properties,
107+
}
108+
if child_kind:
109+
payload["kind"] = child_kind
110+
111+
children.append(payload)
112+
113+
return children
114+
except Exception: # best-effort only
115+
logger.debug("Failed to load configuration children from template '%s'", template_path)
116+
return []
117+
118+
34119
def _get_deployment_ops(cli, deployment_name: str, resource_group: str) -> list[dict] | None:
35120
"""Return ARM deployment operations for a group deployment.
36121
@@ -62,58 +147,39 @@ def _get_deployment_ops(cli, deployment_name: str, resource_group: str) -> list[
62147
return None
63148

64149

65-
def _pick_failed_ops(ops: list[dict] | None) -> list[dict] | None:
150+
def _summarize_deployment_ops(ops: list[dict] | None) -> tuple[str | None, str | None, str | None, list[dict]]:
151+
"""Return (site_id, config_id, config_ref_id, failed_ops) (best-effort)."""
152+
site_id = None
153+
config_id = None
154+
config_ref_id = None
155+
failed_ops: list[dict] = []
156+
66157
if not isinstance(ops, list):
67-
return None
158+
return site_id, config_id, config_ref_id, failed_ops
68159

69-
failed = []
70160
for op in ops:
71161
if not isinstance(op, dict):
72162
continue
163+
73164
state = (op.get("state") or "").lower()
74-
if state in ("failed", "canceled"):
75-
failed.append({
165+
if state == "succeeded":
166+
r_type_norm = (op.get("type") or "").lower()
167+
r_id = op.get("id")
168+
if r_type_norm == "microsoft.edge/sites" and not site_id:
169+
site_id = r_id
170+
elif r_type_norm == "microsoft.edge/configurations" and not config_id:
171+
config_id = r_id
172+
elif r_type_norm == "microsoft.edge/configurationreferences" and not config_ref_id:
173+
config_ref_id = r_id
174+
elif state in ("failed", "canceled"):
175+
failed_ops.append({
76176
"type": op.get("type"),
77177
"name": op.get("name"),
78178
"state": op.get("state"),
79179
"statusMessage": op.get("statusMessage"),
80180
})
81181

82-
return failed or None
83-
84-
85-
def _pick_succeeded_ids(ops: list[dict] | None) -> tuple[str | None, str | None, str | None]:
86-
"""Return (site_id, config_id, config_ref_id) for succeeded operations (best-effort)."""
87-
if not isinstance(ops, list):
88-
return None, None, None
89-
90-
site_id = None
91-
config_id = None
92-
config_ref_id = None
93-
94-
for op in ops:
95-
if not isinstance(op, dict):
96-
continue
97-
if (op.get("state") or "").lower() != "succeeded":
98-
continue
99-
100-
r_type = op.get("type")
101-
r_id = op.get("id")
102-
103-
# resourceType casing can vary; normalize comparisons
104-
r_type_norm = (r_type or "").lower()
105-
if r_type_norm == "microsoft.edge/sites" and not site_id:
106-
site_id = r_id
107-
elif r_type_norm == "microsoft.edge/configurations" and not config_id:
108-
config_id = r_id
109-
elif r_type_norm == "microsoft.edge/configurationreferences" and not config_ref_id:
110-
config_ref_id = r_id
111-
112-
return site_id, config_id, config_ref_id
113-
114-
115-
def _arm_id_suffix(arm_id: str | None) -> str:
116-
return f" Azure Resource ID - {arm_id}" if arm_id else ""
182+
return site_id, config_id, config_ref_id, failed_ops
117183

118184

119185
def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str:
@@ -133,10 +199,18 @@ def _create_resource_group(cli, rg_name: str, location_arg: str | None) -> str:
133199
underlying_error = None
134200
if getattr(cli, "result", None) is not None:
135201
underlying_error = getattr(cli.result, "error", None)
136-
msg = f"Failed to create resource group '{rg_name}' in location '{create_loc}'."
202+
203+
az_error = CLIInternalError(
204+
f"Failed to create or update resource group '{rg_name}' in location '{create_loc}'."
205+
)
206+
recommendations = [
207+
"Verify the location is valid, or specify a different one with --location.",
208+
"Verify you are logged in and have permission to create resource groups in the current subscription.",
209+
]
137210
if underlying_error:
138-
msg = f"{msg}\nUnderlying error: {underlying_error}"
139-
raise CLIInternalError(msg)
211+
recommendations.append(f"Review the Azure CLI error details: {underlying_error}")
212+
az_error.set_recommendation(recommendations)
213+
raise az_error
140214

141215
return create_loc
142216

@@ -203,26 +277,28 @@ def _handler(self, command_args):
203277
def handle(self):
204278
template = _resolve_template_path()
205279
if not template.exists():
206-
raise FileOperationError(f"Internal ARM template not found: {template}")
207-
208-
scope_raw = None
280+
az_error = FileOperationError(f"Internal ARM template not found: {template}")
281+
az_error.set_recommendation([
282+
"Reinstall or update the 'site' extension to restore the bundled templates.",
283+
"If you are developing locally, ensure 'templates/infra/main.json' exists under the extension root.",
284+
])
285+
raise az_error
286+
287+
scope = "resource-group"
209288
if has_value(self.ctx.args.scope):
210-
scope_raw = (self.ctx.args.scope.to_serialized_data() or "").strip()
211-
scope = (scope_raw or "resource-group").lower()
289+
scope = (self.ctx.args.scope.to_serialized_data() or "").strip().lower() or "resource-group"
212290
if scope != "resource-group":
213-
raise InvalidArgumentValueError(
214-
"Invalid --scope value. Currently supported: resource-group."
215-
)
291+
az_error = InvalidArgumentValueError("Invalid value for --scope. Only 'resource-group' is supported.")
292+
az_error.set_recommendation("Use --scope resource-group, or omit --scope to use the default.")
293+
raise az_error
216294

217-
cfg_raw = None
295+
configuration = "defaults"
218296
if has_value(self.ctx.args.configuration):
219-
cfg_raw = (self.ctx.args.configuration.to_serialized_data() or "").strip()
220-
if not cfg_raw:
221-
cfg_raw = "defaults"
222-
if cfg_raw.lower() != "defaults":
223-
raise InvalidArgumentValueError(
224-
"Invalid --configuration value. Currently supported: defaults."
225-
)
297+
configuration = (self.ctx.args.configuration.to_serialized_data() or "").strip() or "defaults"
298+
if configuration.lower() != "defaults":
299+
az_error = InvalidArgumentValueError("Invalid value for --configuration. Only 'defaults' is supported.")
300+
az_error.set_recommendation("Use --configuration defaults, or omit --configuration to use the default.")
301+
raise az_error
226302

227303
site_name = self.ctx.args.name.to_serialized_data()
228304
cli = get_default_cli()
@@ -231,12 +307,8 @@ def handle(self):
231307
if has_value(self.ctx.args.location):
232308
location_arg = self.ctx.args.location.to_serialized_data()
233309

234-
if has_value(self.ctx.args.resource_group):
235-
rg = self.ctx.args.resource_group.to_serialized_data()
236-
rg_location = _create_resource_group(cli, rg, location_arg)
237-
else:
238-
rg = f"{site_name}"
239-
rg_location = _create_resource_group(cli, rg, location_arg)
310+
rg = self.ctx.args.resource_group.to_serialized_data() if has_value(self.ctx.args.resource_group) else site_name
311+
rg_location = _create_resource_group(cli, rg, location_arg)
240312

241313
deployment_name = f"site-quickstart-{site_name}"
242314

@@ -262,51 +334,58 @@ def handle(self):
262334
if getattr(cli, "result", None) is not None:
263335
underlying_error = getattr(cli.result, "error", None)
264336

265-
# Single call: list all operations and reuse for both succeeded + failed reporting
266337
all_ops = _get_deployment_ops(cli, deployment_name, rg)
267-
succeeded_site_id, succeeded_config_id, succeeded_config_ref_id = _pick_succeeded_ids(all_ops)
338+
succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops)
268339

269340
# Print succeeded resources even if the overall deployment failed
270341
if succeeded_site_id:
271-
print("Site created successfully." + _arm_id_suffix(succeeded_site_id))
342+
print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}")
272343
if succeeded_config_id:
273-
print("Config created successfully." + _arm_id_suffix(succeeded_config_id))
344+
print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}")
274345
if succeeded_config_ref_id:
275-
print("Config reference created successfully." + _arm_id_suffix(succeeded_config_ref_id))
276-
277-
failed_ops = _pick_failed_ops(all_ops)
278-
279-
# Optional enrichment: fetch the top-level deployment error only when ops aren't available
280-
deployment_error = None
281-
if not failed_ops:
282-
try:
283-
show_args = [
284-
"deployment", "group", "show",
285-
"--name", deployment_name,
286-
"--resource-group", rg,
287-
"--only-show-errors",
288-
"--query", "properties.error",
289-
"--output", "json",
290-
]
291-
cli.invoke(show_args)
292-
if getattr(cli, "result", None) is not None:
293-
deployment_error = cli.result.result
294-
except Exception:
295-
deployment_error = None
296-
297-
msg = (
298-
"ARM deployment failed for site quickstart. "
299-
f"Deployment name: {deployment_name}, resource group: {rg}."
346+
print(f"Config reference created successfully. Azure Resource ID - {succeeded_config_ref_id}")
347+
348+
az_error = CLIInternalError(
349+
f"Deployment failed to create all required resources. Deployment name '{deployment_name}', resource group '{rg}'."
300350
)
301-
if underlying_error:
302-
msg = f"{msg}\nUnderlying error: {underlying_error}"
303351

304-
if deployment_error:
305-
msg = f"{msg}\nDeployment error:\n{json.dumps(deployment_error, indent=2)}"
352+
recommendations = [
353+
f"Run: az deployment group show --resource-group {rg} --name {deployment_name} --query properties.error --output jsonc",
354+
f"Run: az deployment operation group list --resource-group {rg} --name {deployment_name} --output table",
355+
]
306356

307357
if failed_ops:
308-
msg = f"{msg}\nFailed operations:\n{json.dumps(failed_ops, indent=2)}"
358+
failed_summary = "; ".join(
359+
f"{op.get('type')} '{op.get('name')}' ({op.get('state')})"
360+
for op in failed_ops
361+
if isinstance(op, dict)
362+
)
363+
if failed_summary:
364+
recommendations.append(f"Review failed resources: {failed_summary}")
309365

310-
raise CLIInternalError(msg)
366+
if succeeded_site_id or succeeded_config_id or succeeded_config_ref_id:
367+
recommendations.append("Some resources may have been created. Review the resource group resources and clean up if needed.")
311368

312-
return None
369+
if underlying_error:
370+
recommendations.append(f"Review the Azure CLI error details: {underlying_error}")
371+
372+
az_error.set_recommendation(recommendations)
373+
raise az_error
374+
375+
# Success: return structured output (JSON by default).
376+
all_ops = _get_deployment_ops(cli, deployment_name, rg)
377+
site_id, config_id, config_ref_id, _ = _summarize_deployment_ops(all_ops)
378+
379+
child_configs = _load_template_configuration_children(template, config_id)
380+
381+
return {
382+
"siteId": site_id,
383+
"siteName": site_name,
384+
"type": "Microsoft.Edge/sites",
385+
"siteConfiguration": {
386+
"configurationId": config_id,
387+
"location": rg_location,
388+
"childConfigurations": child_configs,
389+
"configurationReferenceId": config_ref_id,
390+
},
391+
}

0 commit comments

Comments
 (0)