@@ -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+
34119def _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
119185def _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 } \n Underlying 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 } \n Underlying error: { underlying_error } "
303351
304- if deployment_error :
305- msg = f"{ msg } \n Deployment 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 } \n Failed 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