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 @@ -60,6 +60,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 @@ -115,7 +115,8 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA
"resourceInfoCodegen": false,
"userDefinedConstraints": false,
"deployCommands": false,
"patch": false
"patch": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -201,7 +202,8 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"patch": false
"patch": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -309,7 +311,8 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"patch": false
"patch": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -395,7 +398,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
ModuleExtensionConfigs: false,
UserDefinedConstraints: false,
DeployCommands: false,
Patch: false);
Patch: false,
RuntimeValuesInTagsAndSku: false);

configuration.WithExperimentalFeaturesEnabled(experimentalFeaturesEnabled).Should().HaveContents(/*lang=json,strict*/ """
{
Expand Down Expand Up @@ -482,7 +486,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"patch": false
"patch": false,
"runtimeValuesInTagsAndSku": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -838,7 +843,8 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"patch": false
"patch": 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 @@ -25,7 +25,8 @@ public record FeatureProviderOverrides(
bool? ModuleExtensionConfigsEnabled = default,
bool? UserDefinedConstraintsEnabled = default,
bool? DeployCommandsEnabled = default,
bool? PatchEnabled = default)
bool? PatchEnabled = default,
bool? RuntimeValuesInTagsAndSkuEnabled = default)
{
public FeatureProviderOverrides(
TestContext testContext,
Expand All @@ -45,7 +46,8 @@ public FeatureProviderOverrides(
bool? ModuleExtensionConfigsEnabled = default,
bool? UserDefinedConstraintsEnabled = default,
bool? DeployCommandsEnabled = default,
bool? PatchEnabled = default) : this(
bool? PatchEnabled = default,
bool? RuntimeValuesInTagsAndSkuEnabled = default) : this(
FileHelper.GetCacheRootDirectory(testContext),
RegistryEnabled,
OciEnabled,
Expand All @@ -63,7 +65,8 @@ public FeatureProviderOverrides(
ModuleExtensionConfigsEnabled,
UserDefinedConstraintsEnabled,
DeployCommandsEnabled,
PatchEnabled)
PatchEnabled,
RuntimeValuesInTagsAndSkuEnabled)
{ }
}

Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ public OverriddenFeatureProvider(IFeatureProvider features, FeatureProviderOverr
public bool DeployCommandsEnabled => overrides.DeployCommandsEnabled ?? features.DeployCommandsEnabled;

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

public bool RuntimeValuesInTagsAndSkuEnabled => overrides.RuntimeValuesInTagsAndSkuEnabled ?? features.RuntimeValuesInTagsAndSkuEnabled;
}
150 changes: 150 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,155 @@ 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')))]");
template.Should().HaveValueAtPath("$.languageVersion", "2.1-experimental");
}
}

[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')]");
template.Should().HaveValueAtPath("$.languageVersion", "2.1-experimental");
}
}

[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')))]");
template.Should().HaveValueAtPath("$.languageVersion", "2.1-experimental");
}
}
}
}
6 changes: 4 additions & 2 deletions src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public record ExperimentalFeaturesEnabled(
bool ModuleExtensionConfigs,
bool UserDefinedConstraints,
bool DeployCommands,
bool Patch)
bool Patch,
bool RuntimeValuesInTagsAndSku)
{
public static ExperimentalFeaturesEnabled Bind(JsonElement element)
=> element.ToNonNullObject<ExperimentalFeaturesEnabled>();
Expand All @@ -44,5 +45,6 @@ public static ExperimentalFeaturesEnabled Bind(JsonElement element)
ModuleExtensionConfigs: false,
UserDefinedConstraints: false,
DeployCommands: false,
Patch: false);
Patch: 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 @@ -56,6 +56,8 @@ public FeatureProvider(RootConfiguration configuration, IFileExplorer fileExplor

public bool PatchEnabled => configuration.ExperimentalFeaturesEnabled.Patch;

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 @@ -39,6 +39,8 @@ public interface IFeatureProvider

bool PatchEnabled { get; }

bool RuntimeValuesInTagsAndSkuEnabled { get; }

IEnumerable<(string name, bool impactsCompilation, bool usesExperimentalArmEngineFeature)> EnabledFeatureMetadata
{
get
Expand All @@ -59,6 +61,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 @@ -26,5 +26,6 @@ public class RecordBasedFeatureProvider(ExperimentalFeaturesEnabled features) :
public bool UserDefinedConstraintsEnabled => features.UserDefinedConstraints;
public bool DeployCommandsEnabled => features.DeployCommands;
public bool PatchEnabled => features.Patch;
public bool RuntimeValuesInTagsAndSkuEnabled => features.RuntimeValuesInTagsAndSku;
}
}
5 changes: 5 additions & 0 deletions src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,11 @@ private TypeSymbol GetResourceTypeFromString(TextSpan span, string stringContent
flags |= ResourceTypeGenerationFlags.HasParentDefined;
}

if (features.RuntimeValuesInTagsAndSkuEnabled)
{
flags |= ResourceTypeGenerationFlags.PermitRuntimeValuesInTagsAndSku;
}

return (flags, parentType as ResourceType);
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeProvider.cs
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 Expand Up @@ -308,11 +310,20 @@ static NamedTypeProperty UpdateFlags(NamedTypeProperty typeProperty, TypePropert
properties = properties.SetItem(LanguageConstants.ResourceScopePropertyName, scopeProperty);
}

var permitRuntimeValuesInTagsAndSku = flags.HasFlag(ResourceTypeGenerationFlags.PermitRuntimeValuesInTagsAndSku);

// TODO: move this to the type library.
foreach (var propertyName in WriteOnlyDeployTimeConstantPropertyNames)
{
if (properties.TryGetValue(propertyName, out var typeProperty))
{
if (permitRuntimeValuesInTagsAndSku &&
(propertyName == ResourceTagsPropertyName || propertyName == ResourceSkuPropertyName))
{
// The experimental feature allows runtime values in 'tags' and 'sku', so skip marking them as deploy-time constant.
continue;
}

// Update tags for deploy-time constant properties that are not readable at deploy-time.
properties = properties.SetItem(propertyName, UpdateFlags(typeProperty, typeProperty.Flags | TypePropertyFlags.DeployTimeConstant));
}
Expand Down
5 changes: 5 additions & 0 deletions src/Bicep.Core/TypeSystem/ResourceTypeGenerationFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,10 @@ public enum ResourceTypeGenerationFlags
/// Generating a definition for a syntactically nested resource. Do not use this flag for resources that need the "parent" property.
/// </summary>
NestedResource = 1 << 2,

/// <summary>
/// Do not mark the top-level 'tags' and 'sku' properties as deploy-time constant, allowing runtime values to be assigned to them.
/// </summary>
PermitRuntimeValuesInTagsAndSku = 1 << 3,
}
}
Loading
Loading