Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/experimental-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Enables Bicep to run deployments locally, so that you can run Bicep extensions w
Moves defining extension configurations to the module level rather than from within a template. The feature also
includes enhancements for Deployment stacks extensibility integration. This feature is not ready for use.

### `patchPolicy`

Enables the `@patchPolicy` decorator for deploying resources using the PATCH HTTP method instead of PUT. This feature is restricted to Azure Policy DeployIfNotExists (DINE) scenarios, allowing policies to make incremental changes to existing resources without full redeployment.

### `resourceInfoCodegen`

Enables the 'resourceInfo' function for simplified code generation.
Expand Down
254 changes: 254 additions & 0 deletions src/Bicep.Core.IntegrationTests/PatchPolicyDecoratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Bicep.Core.Diagnostics;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Features;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests;

[TestClass]
public class PatchPolicyDecoratorTests
{
public TestContext TestContext { get; set; } = null!;

private ServiceBuilder GetServices(bool patchPolicyEnabled = true) =>
new ServiceBuilder().WithFeatureOverrides(new FeatureProviderOverrides(TestContext, PatchPolicyEnabled: patchPolicyEnabled));

[TestMethod]
public void PatchPolicy_Decorator_Should_Compile_Successfully()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy({
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void PatchPolicy_Decorator_Should_Emit_Method_Property()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy({
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
result.Template.Should().NotBeNull();

var resource = result.Template!.SelectToken("$.resources[0]");
resource.Should().NotBeNull();
resource!["method"]?.ToString().Should().Be("PATCH");
}

[TestMethod]
public void PatchPolicy_Decorator_Should_Emit_Properties_From_Decorator_Body()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy({
identity: {
type: 'UserAssigned'
}
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
result.Template.Should().NotBeNull();

var resource = result.Template!.SelectToken("$.resources[0]");
resource.Should().NotBeNull();

// Check that the method is PATCH
resource!["method"]?.ToString().Should().Be("PATCH");

// Check that the identity from decorator body is emitted
resource["identity"]?["type"]?.ToString().Should().Be("UserAssigned");

// Check that the properties from decorator body are emitted
resource["properties"]?["displayName"]?.ToString().Should().Be("Updated Display Name");
}

[TestMethod]
public void PatchPolicy_Decorator_Should_Not_Appear_In_Options()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy({
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
result.Template.Should().NotBeNull();

var resource = result.Template!.SelectToken("$.resources[0]");
resource.Should().NotBeNull();

// patchPolicy should NOT appear in @options
resource!["@options"]?.SelectToken("$.patchPolicy").Should().BeNull();
}

[TestMethod]
public void PatchPolicy_Decorator_Without_Properties_Should_Produce_Error()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy()
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP071", DiagnosticLevel.Error, "Expected 1 argument, but got 0."),
});
}

[TestMethod]
public void PatchPolicy_Decorator_With_Invalid_Parameter_Type_Should_Produce_Error()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy('invalid')
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP070", DiagnosticLevel.Error, "Argument of type \"'invalid'\" is not assignable to parameter of type \"object\"."),
});
}

[TestMethod]
public void PatchPolicy_Decorator_Can_Be_Used_With_Other_Decorators()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@description('My storage account')
@patchPolicy({
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void PatchPolicy_Decorator_On_Non_Resource_Should_Produce_Error()
{
var services = GetServices();
var result = CompilationHelper.Compile(services, """
@patchPolicy({
value: 'test'
})
param myParam string
""");

result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP125", DiagnosticLevel.Error, "Function \"patchPolicy\" cannot be used as a parameter decorator."),
});
}

[TestMethod]
public void PatchPolicy_Decorator_Should_Not_Be_Available_When_Feature_Disabled()
{
var services = GetServices(patchPolicyEnabled: false);
var result = CompilationHelper.Compile(services, """
@patchPolicy({
properties: {
displayName: 'Updated Display Name'
}
})
resource sa 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: 'teststorage'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
""");

// When feature is disabled, patchPolicy should not be recognized
result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP057", DiagnosticLevel.Error, "The name \"patchPolicy\" does not exist in the current context."),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA
"resourceInfoCodegen": false,
"userDefinedConstraints": false,
"deployCommands": false,
"thisNamespace": false
"thisNamespace": false,
"patchPolicy": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -195,7 +196,8 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"thisNamespace": false
"thisNamespace": false,
"patchPolicy": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -300,7 +302,8 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"thisNamespace": false
"thisNamespace": false,
"patchPolicy": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -386,7 +389,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
ModuleExtensionConfigs: false,
UserDefinedConstraints: false,
DeployCommands: false,
ThisNamespace: false);
ThisNamespace: false,
PatchPolicy: false);

configuration.WithExperimentalFeaturesEnabled(experimentalFeaturesEnabled).Should().HaveContents(/*lang=json,strict*/ """
{
Expand Down Expand Up @@ -470,7 +474,8 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"thisNamespace": false
"thisNamespace": false,
"patchPolicy": false
},
"formatting": {
"indentKind": "Space",
Expand Down Expand Up @@ -821,7 +826,8 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat
"moduleExtensionConfigs": false,
"userDefinedConstraints": false,
"deployCommands": false,
"thisNamespace": false
"thisNamespace": false,
"patchPolicy": 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? ThisNamespaceEnabled = default)
bool? ThisNamespaceEnabled = default,
bool? PatchPolicyEnabled = default)
{
public FeatureProviderOverrides(
TestContext testContext,
Expand All @@ -45,7 +46,8 @@ public FeatureProviderOverrides(
bool? ModuleExtensionConfigsEnabled = default,
bool? UserDefinedConstraintsEnabled = default,
bool? DeployCommandsEnabled = default,
bool? ThisNamespaceEnabled = default) : this(
bool? ThisNamespaceEnabled = default,
bool? PatchPolicyEnabled = default) : this(
FileHelper.GetCacheRootDirectory(testContext),
RegistryEnabled,
SymbolicNameCodegenEnabled,
Expand All @@ -63,6 +65,7 @@ public FeatureProviderOverrides(
ModuleExtensionConfigsEnabled,
UserDefinedConstraintsEnabled,
DeployCommandsEnabled,
ThisNamespaceEnabled)
ThisNamespaceEnabled,
PatchPolicyEnabled)
{ }
}
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 ThisNamespaceEnabled => overrides.ThisNamespaceEnabled ?? features.ThisNamespaceEnabled;

public bool PatchPolicyEnabled => overrides.PatchPolicyEnabled ?? features.PatchPolicyEnabled;
}
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 ThisNamespace)
bool ThisNamespace,
bool PatchPolicy)
{
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,
ThisNamespace: false);
ThisNamespace: false,
PatchPolicy: false);
}
Loading
Loading