Skip to content

Commit 4a62196

Browse files
Add ARM template handling and configuration defaults to quickstart command
1 parent 3525f0c commit 4a62196

2 files changed

Lines changed: 139 additions & 83 deletions

File tree

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

Lines changed: 133 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
5-
65
import json
6+
import logging
7+
from contextlib import contextmanager
8+
from importlib import resources as pkg_resources
79
from pathlib import Path
810
from azure.cli.core.aaz import ( # type: ignore[import-unresolved]
911
AAZCommand,
@@ -25,10 +27,48 @@
2527
logger = get_logger(__name__)
2628

2729

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+
2868
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"
3272

3373

3474
def _load_template_configuration_children(template_path: Path, config_id: str | None) -> list[dict]:
@@ -275,15 +315,6 @@ def _handler(self, command_args):
275315
return self.handle()
276316

277317
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-
287318
scope = "resource-group"
288319
if has_value(self.ctx.args.scope):
289320
scope = (self.ctx.args.scope.to_serialized_data() or "").strip().lower() or "resource-group"
@@ -312,80 +343,100 @@ def handle(self):
312343

313344
deployment_name = f"site-quickstart-{site_name}"
314345

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+
]
325357

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+
)
329378

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+
)
336400

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+
]
356405

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}")
365414

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+
)
368419

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}")
371422

372-
az_error.set_recommendation(recommendations)
373-
raise az_error
423+
az_error.set_recommendation(recommendations)
424+
raise az_error
374425

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+
}

src/site/azext_site/templates/infra/main.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@
3333
},
3434
"configuration": {
3535
"type": "object",
36-
"defaultValue": {}
36+
"defaultValue": {
37+
"version": "1.0.0",
38+
"networkConfigName": "[parameters('networkConfigName')]",
39+
"networkConfigurationKind": "[parameters('networkConfigurationKind')]",
40+
"networkConfiguration": "[parameters('networkConfiguration')]"
41+
}
3742
},
3843
"resourceGroupName": {
3944
"type": "string",

0 commit comments

Comments
 (0)