Skip to content
15 changes: 15 additions & 0 deletions docs/experimental-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ Enables the 'resourceInfo' function for simplified code generation.

Enables the type for a parameter or output to be of type resource to make it easier to pass resource references between modules. This feature is only partially implemented. See [Simplifying resource referencing](https://github.com/azure/bicep/issues/2245).

### `runtimeValuesInTagsAndSku`

Allows the use of runtime values (such as this.existingResource()) in `tags` and `sku` properties. By default, these properties are flagged as deploy-time constants, meaning they cannot reference runtime resource properties. Enabling this feature relaxes that restriction. (Note: This feature will not work until the backend service support has been deployed)

```bicep
resource example 'Microsoft...' = {
name: 'example'
location: resourceGroup().location
sku: {
name: this.existingResource().?sku.name ?? 'Standard'
}
tags: this.existingResource().?tags ?? {}
}
```

### `sourceMapping`

Enables basic source mapping to map an error location returned in the ARM template layer back to the relevant location in the Bicep file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA
"localDeploy": false,
"resourceInfoCodegen": false,
"userDefinedConstraints": false,
"deployCommands": false
"deployCommands": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -199,7 +200,8 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat
"resourceInfoCodegen": false,
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false
"deployCommands": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -306,7 +308,8 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration
"resourceInfoCodegen": false,
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false
"deployCommands": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -391,7 +394,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
ResourceInfoCodegen: false,
ModuleExtensionConfigs: false,
UserDefinedConstraints: false,
DeployCommands: false);
DeployCommands: false,
RuntimeValuesInTagsAndSku: false);

configuration.WithExperimentalFeaturesEnabled(experimentalFeaturesEnabled).Should().HaveContents(/*lang=json,strict*/ """
{
Expand Down Expand Up @@ -477,7 +481,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
"resourceInfoCodegen": false,
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false
"deployCommands": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -832,7 +837,8 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat
"resourceInfoCodegen": false,
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false
"deployCommands": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down
9 changes: 6 additions & 3 deletions src/Bicep.Core.UnitTests/Features/FeatureProviderOverrides.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public record FeatureProviderOverrides(
string? AssemblyVersion = BicepTestConstants.DevAssemblyFileVersion,
bool? ModuleExtensionConfigsEnabled = default,
bool? UserDefinedConstraintsEnabled = default,
bool? DeployCommandsEnabled = default)
bool? DeployCommandsEnabled = default,
bool? RuntimeValuesInTagsAndSkuEnabled = default)
{
public FeatureProviderOverrides(
TestContext testContext,
Expand All @@ -43,7 +44,8 @@ public FeatureProviderOverrides(
string? AssemblyVersion = BicepTestConstants.DevAssemblyFileVersion,
bool? ModuleExtensionConfigsEnabled = default,
bool? UserDefinedConstraintsEnabled = default,
bool? DeployCommandsEnabled = default) : this(
bool? DeployCommandsEnabled = default,
bool? RuntimeValuesInTagsAndSkuEnabled = default) : this(
FileHelper.GetCacheRootDirectory(testContext),
RegistryEnabled,
OciEnabled,
Expand All @@ -60,6 +62,7 @@ public FeatureProviderOverrides(
AssemblyVersion,
ModuleExtensionConfigsEnabled,
UserDefinedConstraintsEnabled,
DeployCommandsEnabled)
DeployCommandsEnabled,
RuntimeValuesInTagsAndSkuEnabled)
{ }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ public OverriddenFeatureProvider(IFeatureProvider features, FeatureProviderOverr
public bool UserDefinedConstraintsEnabled => overrides.UserDefinedConstraintsEnabled ?? features.UserDefinedConstraintsEnabled;

public bool DeployCommandsEnabled => overrides.DeployCommandsEnabled ?? features.DeployCommandsEnabled;

public bool RuntimeValuesInTagsAndSkuEnabled => overrides.RuntimeValuesInTagsAndSkuEnabled ?? features.RuntimeValuesInTagsAndSkuEnabled;
}
147 changes: 147 additions & 0 deletions src/Bicep.Core.UnitTests/Semantics/Namespaces/ThisNamespaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,5 +534,152 @@ public void ThisNamespace_ForLoopNamedThis_ShouldSucceed()
template.Should().HaveValueAtPath("$.resources[0].name", "[format('testStorage{0}', range(0, 2)[copyIndex()])]");
}
}

[TestMethod]
public void ThisNamespace_ExistingResourceInTags_WithFeatureEnabled_ShouldSucceed()
{
var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, RuntimeValuesInTagsAndSkuEnabled: true));
var result = CompilationHelper.Compile(services, @"
resource testResource 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'testStorage'
location: 'westus'
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
tags: {
previousAccessTier: this.existingResource().?properties.accessTier ?? 'unknown'
}
properties: {
allowBlobPublicAccess: this.exists()
}
}
");

result.Should().NotHaveAnyDiagnostics();

using (new AssertionScope())
{
var template = result.Template;

template.Should().HaveValueAtPath("$.resources.testResource.tags.previousAccessTier", "[coalesce(tryGet(target('full'), 'properties', 'accessTier'), 'unknown')]");
template.Should().HaveValueAtPath("$.resources.testResource.properties.allowBlobPublicAccess", "[not(empty(target('full')))]");
}
}

[TestMethod]
public void ThisNamespace_ExistingResourceInSku_WithFeatureEnabled_ShouldSucceed()
{
var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, RuntimeValuesInTagsAndSkuEnabled: true));
var result = CompilationHelper.Compile(services, @"
resource testResource 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'testStorage'
location: 'westus'
sku: {
name: this.existingResource().?sku.name ?? 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: this.exists()
}
}
");

result.Should().NotHaveAnyDiagnostics();

using (new AssertionScope())
{
var template = result.Template;

template.Should().HaveValueAtPath("$.resources.testResource.sku.name", "[coalesce(tryGet(target('full'), 'sku', 'name'), 'Standard_LRS')]");
}
}

[TestMethod]
public void ThisNamespace_ExistingResourceInTags_WithFeatureDisabled_ShouldFail()
{
var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, RuntimeValuesInTagsAndSkuEnabled: false));
var result = CompilationHelper.Compile(services, @"
resource testResource 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'testStorage'
location: 'westus'
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
tags: {
previousAccessTier: this.existingResource().?properties.accessTier ?? 'unknown'
}
properties: {
allowBlobPublicAccess: this.exists()
}
}
");

result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP120", DiagnosticLevel.Error, "This expression is being used in an assignment to the \"tags\" property of the \"Microsoft.Storage/storageAccounts\" type, which requires a value that can be calculated at the start of the deployment."),
});
}

[TestMethod]
public void ThisNamespace_ExistingResourceInTagsAndSku_WithFeatureEnabled_OtherDtcPropertiesStillEnforced()
{
var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, RuntimeValuesInTagsAndSkuEnabled: true));
var result = CompilationHelper.Compile(services, @"
resource testResource 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: this.exists() ? 'testStorage' : 'defaultStorage'
location: 'westus'
sku: {
name: this.existingResource().?sku.name ?? 'Standard_LRS'
}
kind: 'StorageV2'
tags: {
previousAccessTier: this.existingResource().?properties.accessTier ?? 'unknown'
}
properties: {
allowBlobPublicAccess: this.exists()
}
}
");

result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP120", DiagnosticLevel.Error, "This expression is being used in an assignment to the \"name\" property of the \"Microsoft.Storage/storageAccounts\" type, which requires a value that can be calculated at the start of the deployment."),
});
}

[TestMethod]
public void ThisNamespace_ExistingResourceInTagsAndSku_ForLoop_WithFeatureEnabled_ShouldSucceed()
{
var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, RuntimeValuesInTagsAndSkuEnabled: true));
var result = CompilationHelper.Compile(services, @"
resource testResource 'Microsoft.Storage/storageAccounts@2021-04-01' = [for i in range(0, 3): {
name: 'testStorage-${i}'
location: 'westus'
sku: {
name: this.existingResource().?sku.name ?? 'Standard_LRS'
}
kind: 'StorageV2'
tags: {
previousAccessTier: this.existingResource().?properties.accessTier ?? 'unknown'
}
properties: {
allowBlobPublicAccess: this.exists()
}
}]
");

result.Should().NotHaveAnyDiagnostics();

using (new AssertionScope())
{
var template = result.Template;

template.Should().HaveValueAtPath("$.resources.testResource.tags.previousAccessTier", "[coalesce(tryGet(target('full'), 'properties', 'accessTier'), 'unknown')]");
template.Should().HaveValueAtPath("$.resources.testResource.sku.name", "[coalesce(tryGet(target('full'), 'sku', 'name'), 'Standard_LRS')]");
template.Should().HaveValueAtPath("$.resources.testResource.properties.allowBlobPublicAccess", "[not(empty(target('full')))]");
}
}
}
}
6 changes: 4 additions & 2 deletions src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public record ExperimentalFeaturesEnabled(
bool ResourceInfoCodegen,
bool ModuleExtensionConfigs,
bool UserDefinedConstraints,
bool DeployCommands)
bool DeployCommands,
bool RuntimeValuesInTagsAndSku)
{
public static ExperimentalFeaturesEnabled Bind(JsonElement element)
=> element.ToNonNullObject<ExperimentalFeaturesEnabled>();
Expand All @@ -42,5 +43,6 @@ public static ExperimentalFeaturesEnabled Bind(JsonElement element)
ResourceInfoCodegen: false,
ModuleExtensionConfigs: false,
UserDefinedConstraints: false,
DeployCommands: false);
DeployCommands: false,
RuntimeValuesInTagsAndSku: false);
}
2 changes: 2 additions & 0 deletions src/Bicep.Core/Features/FeatureProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public FeatureProvider(RootConfiguration configuration, IFileExplorer fileExplor

public bool DeployCommandsEnabled => configuration.ExperimentalFeaturesEnabled.DeployCommands;

public bool RuntimeValuesInTagsAndSkuEnabled => configuration.ExperimentalFeaturesEnabled.RuntimeValuesInTagsAndSku;

private static bool ReadBooleanEnvVar(string envVar, bool defaultValue)
=> bool.TryParse(Environment.GetEnvironmentVariable(envVar), out var value) ? value : defaultValue;

Expand Down
3 changes: 3 additions & 0 deletions src/Bicep.Core/Features/IFeatureProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public interface IFeatureProvider

bool DeployCommandsEnabled { get; }

bool RuntimeValuesInTagsAndSkuEnabled { get; }

IEnumerable<(string name, bool impactsCompilation, bool usesExperimentalArmEngineFeature)> EnabledFeatureMetadata
{
get
Expand All @@ -57,6 +59,7 @@ public interface IFeatureProvider
(ModuleExtensionConfigsEnabled, "Enable defining extension configs for modules", true, true),
(UserDefinedConstraintsEnabled, "Enable @validate() decorator", true, true),
(DeployCommandsEnabled, "Enable deploy commands", true, true),
(RuntimeValuesInTagsAndSkuEnabled, "Enable runtime values in tags and SKU", true, true),
})
{
if (enabled)
Expand Down
1 change: 1 addition & 0 deletions src/Bicep.Core/Features/RecordBasedFeatureProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public class RecordBasedFeatureProvider(ExperimentalFeaturesEnabled features) :
public bool ModuleExtensionConfigsEnabled => features.ModuleExtensionConfigs;
public bool UserDefinedConstraintsEnabled => features.UserDefinedConstraints;
public bool DeployCommandsEnabled => features.DeployCommands;
public bool RuntimeValuesInTagsAndSkuEnabled => features.RuntimeValuesInTagsAndSku;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using Bicep.Core.TypeSystem.Providers.Az;
using Bicep.Core.TypeSystem.Types;

namespace Bicep.Core.TypeSystem
Expand Down Expand Up @@ -64,6 +65,14 @@ public override void VisitObjectPropertySyntax(ObjectPropertySyntax syntax)
if (syntax.TryGetTypeProperty(this.semanticModel) is { } typeProperty &&
typeProperty.Flags.HasFlag(TypePropertyFlags.DeployTimeConstant))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to avoid setting this type property in the first place, rather than adding exceptions in the validation code?

One example of why this is problematic / difficult to do here - you're just looking for any property named "sku" or "tags", without checking where exactly it appears - whereas in reality, this should only affect az resource types, and for properties declared as top-level resource properties.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks. Moved the check to AzResourceTypeProvider where the DTC flag is applied. I added a PermitRuntimeValuesInTagsAndSku flag since I didn't see a good way to access FeatureProvider from inside AzResourceTypeProvider.

{
// Skip DTC validation for tags and sku properties when the experimental feature is enabled.
if (this.semanticModel.Features.RuntimeValuesInTagsAndSkuEnabled &&
syntax.TryGetKeyText() is AzResourceTypeProvider.ResourceTagsPropertyName or AzResourceTypeProvider.ResourceSkuPropertyName)
{
Comment thread
Copilot marked this conversation as resolved.
Outdated
base.VisitObjectPropertySyntax(syntax);
return;
}

// The property type exists and and has the DTC flag.
this.deployTimeConstantContainers.Add(syntax);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class AzResourceTypeProvider : ResourceTypeProviderBase, IResourceTypePro
public const string ResourceNamePropertyName = "name";
public const string ResourceTypePropertyName = "type";
public const string ResourceApiVersionPropertyName = "apiVersion";
public const string ResourceTagsPropertyName = "tags";
public const string ResourceSkuPropertyName = "sku";

public const string ResourceTypeDeployments = "Microsoft.Resources/deployments";
public const string ResourceTypeResourceGroup = "Microsoft.Resources/resourceGroups";
Expand Down Expand Up @@ -58,10 +60,10 @@ public static readonly ImmutableSortedSet<string> ReadWriteDeployTimeConstantPro
"extendedLocation",
"zones",
"plan",
"sku",
ResourceSkuPropertyName,
"identity",
"managedByExtended",
"tags",
ResourceTagsPropertyName,
"asserts",
];

Expand Down
4 changes: 4 additions & 0 deletions src/vscode-bicep/schemas/bicepconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,10 @@
"deployCommands": {
"type": "boolean",
"description": "Allows you to access the 'deploy', 'what-if' and 'teardown' commands. See https://aka.ms/bicep/experimental-features#deploycommands"
},
"runtimeValuesInTagsAndSku": {
"type": "boolean",
"description": "Allows the use of runtime values (such as references to resources or this.existingResource()) in tags and sku properties. See https://aka.ms/bicep/experimental-features#runtimevaluesintagsandsku"
}
},
"additionalProperties": false
Expand Down
Loading