|
2 | 2 | # Copyright (c) Microsoft Corporation. All rights reserved. |
3 | 3 | # Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | # -------------------------------------------------------------------------------------------- |
5 | | - |
6 | 5 | import json |
| 6 | +import logging |
| 7 | +from contextlib import contextmanager |
| 8 | +from importlib import resources as pkg_resources |
7 | 9 | from pathlib import Path |
8 | 10 | from azure.cli.core.aaz import ( # type: ignore[import-unresolved] |
9 | 11 | AAZCommand, |
|
25 | 27 | logger = get_logger(__name__) |
26 | 28 |
|
27 | 29 |
|
| 30 | +_TEMPLATE_RESOURCE = ("templates", "infra", "main.json") |
| 31 | + |
| 32 | + |
| 33 | +@contextmanager |
| 34 | +def _template_file(): |
| 35 | + """Yield the bundled ARM template as a real file path. |
| 36 | +
|
| 37 | + This avoids relying on repo-relative or local developer filesystem layout. |
| 38 | + """ |
| 39 | + try: |
| 40 | + trav = pkg_resources.files("azext_site").joinpath(*_TEMPLATE_RESOURCE) |
| 41 | + with pkg_resources.as_file(trav) as template_path: |
| 42 | + yield template_path |
| 43 | + except FileNotFoundError as ex: |
| 44 | + az_error = FileOperationError( |
| 45 | + f"Internal ARM template not found in extension package: {'/'.join(_TEMPLATE_RESOURCE)}" |
| 46 | + ) |
| 47 | + az_error.set_recommendation([ |
| 48 | + "Reinstall or update the 'site' extension to restore the bundled templates.", |
| 49 | + f"Details: {ex}", |
| 50 | + ]) |
| 51 | + raise az_error |
| 52 | + |
| 53 | + |
| 54 | +def _get_configuration_defaults_version(template_path: Path) -> str | None: |
| 55 | + """Best-effort: read parameters.configuration.defaultValue.version from main.json.""" |
| 56 | + try: |
| 57 | + data = json.loads(template_path.read_text(encoding="utf-8")) |
| 58 | + params = data.get("parameters") if isinstance(data, dict) else None |
| 59 | + params = params if isinstance(params, dict) else {} |
| 60 | + cfg = params.get("configuration") if isinstance(params.get("configuration"), dict) else {} |
| 61 | + dv = cfg.get("defaultValue") if isinstance(cfg.get("defaultValue"), dict) else {} |
| 62 | + ver = dv.get("version") |
| 63 | + return ver if isinstance(ver, str) and ver else None |
| 64 | + except Exception: |
| 65 | + return None |
| 66 | + |
| 67 | + |
28 | 68 | def _resolve_template_path() -> Path: |
29 | | - # ...\azext_site\aaz\latest\site\_quickstart.py -> ...\azext_site\templates\infra\main.json |
30 | | - azext_root = Path(__file__).resolve().parents[3] # ...\azext_site |
31 | | - return azext_root / "templates" / "infra" / "main.json" |
| 69 | + # Back-compat helper retained for callers/tests; quickstart should use _template_file(). |
| 70 | + # This path is not used for deployment in order to avoid local filesystem assumptions. |
| 71 | + return Path("templates") / "infra" / "main.json" |
32 | 72 |
|
33 | 73 |
|
34 | 74 | def _load_template_configuration_children(template_path: Path, config_id: str | None) -> list[dict]: |
@@ -275,15 +315,6 @@ def _handler(self, command_args): |
275 | 315 | return self.handle() |
276 | 316 |
|
277 | 317 | def handle(self): |
278 | | - template = _resolve_template_path() |
279 | | - if not template.exists(): |
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 | 318 | scope = "resource-group" |
288 | 319 | if has_value(self.ctx.args.scope): |
289 | 320 | scope = (self.ctx.args.scope.to_serialized_data() or "").strip().lower() or "resource-group" |
@@ -312,80 +343,100 @@ def handle(self): |
312 | 343 |
|
313 | 344 | deployment_name = f"site-quickstart-{site_name}" |
314 | 345 |
|
315 | | - invoke_args = [ |
316 | | - "deployment", "group", "create", |
317 | | - "--name", deployment_name, |
318 | | - "--resource-group", rg, |
319 | | - "--template-file", str(template), |
320 | | - "--parameters", f"siteName={site_name}", |
321 | | - "--parameters", f"location={rg_location}", |
322 | | - "--only-show-errors", |
323 | | - "--output", "none", |
324 | | - ] |
| 346 | + with _template_file() as template: |
| 347 | + invoke_args = [ |
| 348 | + "deployment", "group", "create", |
| 349 | + "--name", deployment_name, |
| 350 | + "--resource-group", rg, |
| 351 | + "--template-file", str(template), |
| 352 | + "--parameters", f"siteName={site_name}", |
| 353 | + "--parameters", f"location={rg_location}", |
| 354 | + "--only-show-errors", |
| 355 | + "--output", "none", |
| 356 | + ] |
325 | 357 |
|
326 | | - if has_value(self.ctx.args.config_name): |
327 | | - cfg = self.ctx.args.config_name.to_serialized_data() |
328 | | - invoke_args.extend(["--parameters", f"configName={cfg}"]) |
| 358 | + config_name = None |
| 359 | + if has_value(self.ctx.args.config_name): |
| 360 | + config_name = self.ctx.args.config_name.to_serialized_data() |
| 361 | + invoke_args.extend(["--parameters", f"configName={config_name}"]) |
| 362 | + |
| 363 | + if logger.isEnabledFor(logging.DEBUG): |
| 364 | + defaults_version = _get_configuration_defaults_version(template) |
| 365 | + logger.debug("Quickstart configuration defaults version: %s", defaults_version) |
| 366 | + logger.debug( |
| 367 | + "Quickstart effective deployment parameters: %s", |
| 368 | + json.dumps( |
| 369 | + { |
| 370 | + "siteName": site_name, |
| 371 | + "location": rg_location, |
| 372 | + **({"configName": config_name} if config_name else {}), |
| 373 | + }, |
| 374 | + indent=2, |
| 375 | + sort_keys=True, |
| 376 | + ), |
| 377 | + ) |
329 | 378 |
|
330 | | - rc = cli.invoke(invoke_args) |
331 | | - if rc != 0: |
332 | | - # Capture the original error first (before more invokes overwrite cli.result) |
333 | | - underlying_error = None |
334 | | - if getattr(cli, "result", None) is not None: |
335 | | - underlying_error = getattr(cli.result, "error", None) |
| 379 | + rc = cli.invoke(invoke_args) |
| 380 | + if rc != 0: |
| 381 | + # Capture the original error first (before more invokes overwrite cli.result) |
| 382 | + underlying_error = None |
| 383 | + if getattr(cli, "result", None) is not None: |
| 384 | + underlying_error = getattr(cli.result, "error", None) |
| 385 | + |
| 386 | + all_ops = _get_deployment_ops(cli, deployment_name, rg) |
| 387 | + succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops) |
| 388 | + |
| 389 | + # Print succeeded resources even if the overall deployment failed |
| 390 | + if succeeded_site_id: |
| 391 | + print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}") |
| 392 | + if succeeded_config_id: |
| 393 | + print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}") |
| 394 | + if succeeded_config_ref_id: |
| 395 | + print(f"Config reference created successfully. Azure Resource ID - {succeeded_config_ref_id}") |
| 396 | + |
| 397 | + az_error = CLIInternalError( |
| 398 | + f"Deployment failed to create all required resources. Deployment name '{deployment_name}', resource group '{rg}'." |
| 399 | + ) |
336 | 400 |
|
337 | | - all_ops = _get_deployment_ops(cli, deployment_name, rg) |
338 | | - succeeded_site_id, succeeded_config_id, succeeded_config_ref_id, failed_ops = _summarize_deployment_ops(all_ops) |
339 | | - |
340 | | - # Print succeeded resources even if the overall deployment failed |
341 | | - if succeeded_site_id: |
342 | | - print(f"Site created successfully. Azure Resource ID - {succeeded_site_id}") |
343 | | - if succeeded_config_id: |
344 | | - print(f"Config created successfully. Azure Resource ID - {succeeded_config_id}") |
345 | | - if succeeded_config_ref_id: |
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}'." |
350 | | - ) |
351 | | - |
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 | | - ] |
| 401 | + recommendations = [ |
| 402 | + f"Run: az deployment group show --resource-group {rg} --name {deployment_name} --query properties.error --output jsonc", |
| 403 | + f"Run: az deployment operation group list --resource-group {rg} --name {deployment_name} --output table", |
| 404 | + ] |
356 | 405 |
|
357 | | - if failed_ops: |
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}") |
| 406 | + if failed_ops: |
| 407 | + failed_summary = "; ".join( |
| 408 | + f"{op.get('type')} '{op.get('name')}' ({op.get('state')})" |
| 409 | + for op in failed_ops |
| 410 | + if isinstance(op, dict) |
| 411 | + ) |
| 412 | + if failed_summary: |
| 413 | + recommendations.append(f"Review failed resources: {failed_summary}") |
365 | 414 |
|
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.") |
| 415 | + if succeeded_site_id or succeeded_config_id or succeeded_config_ref_id: |
| 416 | + recommendations.append( |
| 417 | + "Some resources may have been created. Review the resource group resources and clean up if needed." |
| 418 | + ) |
368 | 419 |
|
369 | | - if underlying_error: |
370 | | - recommendations.append(f"Review the Azure CLI error details: {underlying_error}") |
| 420 | + if underlying_error: |
| 421 | + recommendations.append(f"Review the Azure CLI error details: {underlying_error}") |
371 | 422 |
|
372 | | - az_error.set_recommendation(recommendations) |
373 | | - raise az_error |
| 423 | + az_error.set_recommendation(recommendations) |
| 424 | + raise az_error |
374 | 425 |
|
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 | | - } |
| 426 | + # Success: return structured output (JSON by default). |
| 427 | + all_ops = _get_deployment_ops(cli, deployment_name, rg) |
| 428 | + site_id, config_id, config_ref_id, _ = _summarize_deployment_ops(all_ops) |
| 429 | + |
| 430 | + child_configs = _load_template_configuration_children(template, config_id) |
| 431 | + |
| 432 | + return { |
| 433 | + "siteId": site_id, |
| 434 | + "siteName": site_name, |
| 435 | + "type": "Microsoft.Edge/sites", |
| 436 | + "siteConfiguration": { |
| 437 | + "configurationId": config_id, |
| 438 | + "location": rg_location, |
| 439 | + "childConfigurations": child_configs, |
| 440 | + "configurationReferenceId": config_ref_id, |
| 441 | + }, |
| 442 | + } |
0 commit comments