Skip to content
Merged
22 changes: 22 additions & 0 deletions src/azure-cli/azure/cli/command_modules/resource/_bicep.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,28 @@ def is_bicepparam_file(file_path):
return file_path.lower().endswith(".bicepparam") if file_path else False


def is_using_none_bicepparam_file(file_path):
"""Check if a .bicepparam file uses 'using none' declaration."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except IOError:
return False

# Remove block comments (/* ... */) and single-line comments (// ...)
# so that the first remaining non-empty line reflects the first statement.
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
content = re.sub(r'//.*', '', content)

for line in content.splitlines():
stripped = line.strip()
if not stripped:
continue
# The 'using' declaration must be the first non-comment, non-empty statement
return re.fullmatch(r'using\s+none', stripped, re.IGNORECASE) is not None
return False


def get_bicep_available_release_tags():
try:
os.environ.setdefault("CURL_CA_BUNDLE", certifi.where())
Expand Down
37 changes: 33 additions & 4 deletions src/azure-cli/azure/cli/command_modules/resource/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
run_bicep_command,
is_bicep_file,
is_bicepparam_file,
is_using_none_bicepparam_file,
ensure_bicep_installation,
remove_bicep_installation,
get_bicep_latest_release_tag,
Expand Down Expand Up @@ -1087,10 +1088,26 @@ def _parse_bicepparam_file(cmd, template_file, parameters):

bicepparam_file = _get_bicepparam_file_path(parameters)

if template_file and not is_bicep_file(template_file):
using_none = is_using_none_bicepparam_file(bicepparam_file)

if not using_none and template_file and not is_bicep_file(template_file):
raise ArgumentUsageError("Only a .bicep template is allowed with a .bicepparam file")

template_content, template_spec_id, parameters_content = _build_bicepparam_file(cmd.cli_ctx, bicepparam_file, template_file)
if using_none and not template_file:
raise ArgumentUsageError(
"The .bicepparam file uses 'using none', so a --template-file (-f) must be provided.")

if using_none:
# For 'using none', build params without --bicep-file and build template separately
_, _, parameters_content = _build_bicepparam_file(cmd.cli_ctx, bicepparam_file, None)
template_content = (
run_bicep_command(cmd.cli_ctx, ["build", "--stdout", template_file])
if is_bicep_file(template_file)
else read_file_content(template_file)
)
template_spec_id = None
else:
template_content, template_spec_id, parameters_content = _build_bicepparam_file(cmd.cli_ctx, bicepparam_file, template_file)

if _get_parameter_count(parameters) > 1:
template_obj = None
Expand Down Expand Up @@ -1197,8 +1214,11 @@ def _prepare_deployment_properties_unmodified(cmd, deployment_scope, template_fi
if template_spec_id:
template_link = TemplateLink(id=template_spec_id)
template_obj = _load_template_spec_template(cmd, template_spec_id)
else:
elif template_content:
template_obj = _remove_comments_from_json(template_content)
else:
# 'using none' with separate template file
template_content, template_obj = _process_template_file(cmd, template_file, deployment_scope)

template_schema = template_obj.get('$schema', '')
validate_bicep_target_scope(template_schema, deployment_scope)
Expand Down Expand Up @@ -1397,9 +1417,18 @@ def _prepare_stacks_templates_and_parameters(cmd, rcf, deployment_scope, deploym
if template_spec_id:
template_obj = _load_template_spec_template(cmd, template_spec_id)
deployment_stack_model.template_link = DeploymentStacksTemplateLink(id=template_spec_id)
else:
elif template_content:
template_obj = _remove_comments_from_json(template_content)
deployment_stack_model.template = json.loads(json.dumps(template_obj))
else:
# 'using none' with separate template file
template_content = (
run_bicep_command(cmd.cli_ctx, ["build", "--stdout", template_file])
if is_bicep_file(template_file)
else read_file_content(template_file)
)
template_obj = _remove_comments_from_json(template_content, file_path=template_file)
deployment_stack_model.template = json.loads(json.dumps(template_obj))

template_schema = template_obj.get('$schema', '')
validate_bicep_target_scope(template_schema, deployment_scope)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"type": "string"
},
"kind": {
"type": "string"
}
},
"variables": {
"storageAccountName": "[format('clistore{0}', uniqueString(resourceGroup().id))]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-04-01",
"name": "[variables('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "Standard_LRS"
},
"kind": "[parameters('kind')]",
"properties": {}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using none

param location = 'westus2'

param kind = 'StorageV2'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This is a comment
// Another comment

using none

param location = 'westus2'

param kind = 'StorageV2'
Original file line number Diff line number Diff line change
Expand Up @@ -5967,6 +5967,150 @@ def test_resource_deployment_with_misspelled_bicepparam_file(self):
with self.assertRaisesRegex(CLIError, "Please enter one of the following: template file, template spec, template url, or Bicep parameters file."):
self.cmd('deployment group create --resource-group {rg} --parameters {params}')

def test_is_using_none_bicepparam_file(self):
from azure.cli.command_modules.resource._bicep import is_using_none_bicepparam_file
import tempfile

def _write_temp(content):
f = tempfile.NamedTemporaryFile(mode='w', suffix='.bicepparam', delete=False)
f.write(content)
f.close()
return f.name

# Basic 'using none'
path = _write_temp("using none\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# With leading comments
path = _write_temp("// comment\n// another\nusing none\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# With leading blank lines
path = _write_temp("\n\n \nusing none\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# Case insensitive
path = _write_temp("Using None\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

path = _write_temp("USING NONE\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# Normal using declaration
path = _write_temp("using './main.bicep'\nparam location = 'westus2'\n")
self.assertFalse(is_using_none_bicepparam_file(path))
os.unlink(path)

# Using with another path
path = _write_temp("using 'other.bicep'\nparam location = 'westus2'\n")
self.assertFalse(is_using_none_bicepparam_file(path))
os.unlink(path)

# Block comment before using none
path = _write_temp("/* header comment */\nusing none\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# Multi-line block comment before using none
path = _write_temp("/*\n * Multi-line\n * comment\n */\nusing none\nparam location = 'westus2'\n")
self.assertTrue(is_using_none_bicepparam_file(path))
os.unlink(path)

# Block comment on same line as using none (should not match)
path = _write_temp("/* comment */ using './main.bicep'\nparam location = 'westus2'\n")
self.assertFalse(is_using_none_bicepparam_file(path))
os.unlink(path)

# Test data file with comments (on-disk fixture)
curr_dir = os.path.dirname(os.path.realpath(__file__))
comments_file = os.path.join(curr_dir, 'data', 'bicepparam', 'using_none_with_comments_params.bicepparam')
self.assertTrue(is_using_none_bicepparam_file(comments_file))

# Non-existent file
self.assertFalse(is_using_none_bicepparam_file('/nonexistent/path.bicepparam'))

@ResourceGroupPreparer(name_prefix='cli_test_deployment_with_bicepparam_using_none')
def test_resource_group_level_deployment_with_bicepparam_using_none(self):
curr_dir = os.path.dirname(os.path.realpath(__file__))
self.kwargs.update({
'tf': os.path.join(curr_dir, 'data', 'bicepparam', 'storage_account_template.bicep'),
'params': os.path.join(curr_dir, 'data', 'bicepparam', 'using_none_params.bicepparam')
})

self.cmd('deployment group validate --resource-group {rg} --template-file "{tf}" --parameters {params}', checks=[
self.check('properties.provisioningState', 'Succeeded')
])

self.cmd('deployment group what-if --resource-group {rg} --template-file "{tf}" --parameters {params} --no-pretty-print', checks=[
self.check('status', 'Succeeded'),
])

self.cmd('deployment group create --resource-group {rg} --template-file "{tf}" --parameters {params}', checks=[
self.check('properties.provisioningState', 'Succeeded')
])

@ResourceGroupPreparer(name_prefix='cli_test_deployment_with_bicepparam_using_none_json')
def test_resource_group_level_deployment_with_bicepparam_using_none_and_json_template(self):
curr_dir = os.path.dirname(os.path.realpath(__file__))
self.kwargs.update({
'tf': os.path.join(curr_dir, 'data', 'bicepparam', 'storage_account_template.json'),
'params': os.path.join(curr_dir, 'data', 'bicepparam', 'using_none_params.bicepparam')
})

self.cmd('deployment group validate --resource-group {rg} --template-file "{tf}" --parameters {params}', checks=[
self.check('properties.provisioningState', 'Succeeded')
])

self.cmd('deployment group what-if --resource-group {rg} --template-file "{tf}" --parameters {params} --no-pretty-print', checks=[
self.check('status', 'Succeeded'),
])

self.cmd('deployment group create --resource-group {rg} --template-file "{tf}" --parameters {params}', checks=[
self.check('properties.provisioningState', 'Succeeded')
])

def test_resource_deployment_with_bicepparam_using_none_and_no_template(self):
import tempfile

f = tempfile.NamedTemporaryFile(mode='w', suffix='.bicepparam', delete=False)
f.write("using none\nparam location = 'westus2'\n")
f.close()

self.kwargs.update({
'rg': "exampleGroup",
'params': f.name
})

# The bicepparam file uses 'using none', so the CLI requires --template-file.
with self.assertRaisesRegex(CLIError, "The .bicepparam file uses 'using none', so a --template-file"):
self.cmd('deployment group create --resource-group {rg} --parameters {params}')

os.unlink(f.name)

def test_resource_deployment_with_bicepparam_and_json_template_still_fails_without_using_none(self):
"""Ensure existing behavior: non-using-none bicepparam + .json template still errors."""
import tempfile

f = tempfile.NamedTemporaryFile(mode='w', suffix='.bicepparam', delete=False)
f.write("using './something.bicep'\nparam location = 'westus2'\n")
f.close()

self.kwargs.update({
'rg': "exampleGroup",
'tf': "./main.json",
'params': f.name
})

with self.assertRaisesRegex(CLIError, "Only a .bicep template is allowed with a .bicepparam file"):
self.cmd('deployment group create --resource-group {rg} --template-file "{tf}" --parameters {params}')

os.unlink(f.name)

def test_subscription_level_deployment_with_bicep(self):
curr_dir = os.path.dirname(os.path.realpath(__file__))
self.kwargs.update({
Expand Down