diff --git a/docs/experimental-features.md b/docs/experimental-features.md index ea25dbc45e4..0ad0775cf43 100644 --- a/docs/experimental-features.md +++ b/docs/experimental-features.md @@ -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. diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index 6d485b3ecab..2745f0de72b 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -115,7 +115,8 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA "resourceInfoCodegen": false, "userDefinedConstraints": false, "deployCommands": false, - "patch": false + "patch": false, + "runtimeValuesInTagsAndSku": false }, "formatting": { "indentKind": "Space", @@ -201,7 +202,8 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat "moduleExtensionConfigs": false, "userDefinedConstraints": false, "deployCommands": false, - "patch": false + "patch": false, + "runtimeValuesInTagsAndSku": false }, "formatting": { "indentKind": "Space", @@ -309,7 +311,8 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration "moduleExtensionConfigs": false, "userDefinedConstraints": false, "deployCommands": false, - "patch": false + "patch": false, + "runtimeValuesInTagsAndSku": false }, "formatting": { "indentKind": "Space", @@ -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*/ """ { @@ -482,7 +486,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf "moduleExtensionConfigs": false, "userDefinedConstraints": false, "deployCommands": false, - "patch": false + "patch": false, + "runtimeValuesInTagsAndSku": false }, "formatting": { "indentKind": "Space", @@ -838,7 +843,8 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat "moduleExtensionConfigs": false, "userDefinedConstraints": false, "deployCommands": false, - "patch": false + "patch": false, + "runtimeValuesInTagsAndSku": false }, "formatting": { "indentKind": "Space", diff --git a/src/Bicep.Core.UnitTests/Features/FeatureProviderOverrides.cs b/src/Bicep.Core.UnitTests/Features/FeatureProviderOverrides.cs index aa2cc8ed55f..1a30d36542e 100644 --- a/src/Bicep.Core.UnitTests/Features/FeatureProviderOverrides.cs +++ b/src/Bicep.Core.UnitTests/Features/FeatureProviderOverrides.cs @@ -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, @@ -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, @@ -63,7 +65,8 @@ public FeatureProviderOverrides( ModuleExtensionConfigsEnabled, UserDefinedConstraintsEnabled, DeployCommandsEnabled, - PatchEnabled) + PatchEnabled, + RuntimeValuesInTagsAndSkuEnabled) { } } diff --git a/src/Bicep.Core.UnitTests/Features/OverriddenFeatureProvider.cs b/src/Bicep.Core.UnitTests/Features/OverriddenFeatureProvider.cs index 3e8fc5d503a..9b394aa8724 100644 --- a/src/Bicep.Core.UnitTests/Features/OverriddenFeatureProvider.cs +++ b/src/Bicep.Core.UnitTests/Features/OverriddenFeatureProvider.cs @@ -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; } diff --git a/src/Bicep.Core.UnitTests/Semantics/Namespaces/ThisNamespaceTests.cs b/src/Bicep.Core.UnitTests/Semantics/Namespaces/ThisNamespaceTests.cs index 92fee62738b..6107ad18da2 100644 --- a/src/Bicep.Core.UnitTests/Semantics/Namespaces/ThisNamespaceTests.cs +++ b/src/Bicep.Core.UnitTests/Semantics/Namespaces/ThisNamespaceTests.cs @@ -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"); + } + } } } diff --git a/src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs b/src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs index 2ac85115ac1..6975375e97d 100644 --- a/src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs +++ b/src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs @@ -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(); @@ -44,5 +45,6 @@ public static ExperimentalFeaturesEnabled Bind(JsonElement element) ModuleExtensionConfigs: false, UserDefinedConstraints: false, DeployCommands: false, - Patch: false); + Patch: false, + RuntimeValuesInTagsAndSku: false); } diff --git a/src/Bicep.Core/Features/FeatureProvider.cs b/src/Bicep.Core/Features/FeatureProvider.cs index 94634fcd650..416cb2fa00c 100644 --- a/src/Bicep.Core/Features/FeatureProvider.cs +++ b/src/Bicep.Core/Features/FeatureProvider.cs @@ -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; diff --git a/src/Bicep.Core/Features/IFeatureProvider.cs b/src/Bicep.Core/Features/IFeatureProvider.cs index 7eb63012398..bcb76898658 100644 --- a/src/Bicep.Core/Features/IFeatureProvider.cs +++ b/src/Bicep.Core/Features/IFeatureProvider.cs @@ -39,6 +39,8 @@ public interface IFeatureProvider bool PatchEnabled { get; } + bool RuntimeValuesInTagsAndSkuEnabled { get; } + IEnumerable<(string name, bool impactsCompilation, bool usesExperimentalArmEngineFeature)> EnabledFeatureMetadata { get @@ -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) diff --git a/src/Bicep.Core/Features/RecordBasedFeatureProvider.cs b/src/Bicep.Core/Features/RecordBasedFeatureProvider.cs index 7b58b10f3d5..050dc75f026 100644 --- a/src/Bicep.Core/Features/RecordBasedFeatureProvider.cs +++ b/src/Bicep.Core/Features/RecordBasedFeatureProvider.cs @@ -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; } } diff --git a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs index ed8fe2bb06e..4b00fa1ebf9 100644 --- a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs +++ b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs @@ -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); } } diff --git a/src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeProvider.cs b/src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeProvider.cs index 534198fde34..179d93482b7 100644 --- a/src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeProvider.cs +++ b/src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeProvider.cs @@ -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"; @@ -58,10 +60,10 @@ public static readonly ImmutableSortedSet ReadWriteDeployTimeConstantPro "extendedLocation", "zones", "plan", - "sku", + ResourceSkuPropertyName, "identity", "managedByExtended", - "tags", + ResourceTagsPropertyName, "asserts", ]; @@ -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)); } diff --git a/src/Bicep.Core/TypeSystem/ResourceTypeGenerationFlags.cs b/src/Bicep.Core/TypeSystem/ResourceTypeGenerationFlags.cs index 18b568055f4..a37a087f21a 100644 --- a/src/Bicep.Core/TypeSystem/ResourceTypeGenerationFlags.cs +++ b/src/Bicep.Core/TypeSystem/ResourceTypeGenerationFlags.cs @@ -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. /// NestedResource = 1 << 2, + + /// + /// Do not mark the top-level 'tags' and 'sku' properties as deploy-time constant, allowing runtime values to be assigned to them. + /// + PermitRuntimeValuesInTagsAndSku = 1 << 3, } } diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index d2464c5b8b4..6c073c8c3d1 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -998,6 +998,10 @@ "patch": { "type": "boolean", "description": "Enables the @patch() decorator for deploying resources using the PATCH HTTP method. This feature is restricted to Azure Policy DeployIfNotExists scenarios. See https://aka.ms/bicep/experimental-features#patch" + }, + "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