Skip to content

Commit

Permalink
Fixed generating scope for looped modules and nested resources (#4639)
Browse files Browse the repository at this point in the history
* Fix using index value in module scope

* Fix scopes to looped child resources

* Fix variable name

* Renamed expression emitting function
  • Loading branch information
miqm committed Sep 28, 2021
1 parent a0694b3 commit 1e9e336
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 33 deletions.
226 changes: 226 additions & 0 deletions src/Bicep.Core.IntegrationTests/Scenarios/ScopeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests.Scenarios
{
[TestClass]
public class ScopeTests
{
[TestMethod]
public void UsingIndexValueInModuleScope_ShouldInlineTheResourceGroupName()
{
var result = CompilationHelper.Compile(("main.bicep", @"
targetScope = 'subscription'
// parameters
param tags object
param location string
// variables
var resourceGroups = [
'test-uat-rg'
'test-uat-blue-rg'
'test-uat-green-rg'
]
// base resource group deployment
resource rgs 'Microsoft.Resources/resourceGroups@2021-04-01' = [for rgname in resourceGroups: {
name: rgname
location: location
tags: tags
}]
module storage 'storage.bicep' = {
name: 'str'
scope: rgs[0]
params: {
environment: 'uat'
}
}
"), ("storage.bicep", @"
param environment string
output env string = environment
"));

result.Should().NotHaveAnyDiagnostics();
using (new AssertionScope())
{
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'str')].resourceGroup", "[variables('resourceGroups')[0]]");
}
}

[TestMethod]
public void UsingIndexValueInModuleScope_ShouldInlineTheManagementGroupName_1()
{
var result = CompilationHelper.Compile(("main.bicep", @"
targetScope = 'tenant'
// variables
var managementGroups = [
'mg1'
'mg2'
]
resource mgs 'Microsoft.Management/managementGroups@2020-02-01' = [for mgname in managementGroups: {
name: mgname
}]
module mod 'mod.bicep' = {
name: 'mod'
scope: mgs[0]
params: {
environment: 'uat'
}
}
"), ("mod.bicep", @"
targetScope = 'managementGroup'
param environment string
output env string = environment
"));

result.Should().NotHaveAnyDiagnostics();
using (new AssertionScope())
{
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'mod')].scope", "[format('Microsoft.Management/managementGroups/{0}', variables('managementGroups')[0])]");
}
}

[TestMethod]
public void UsingIndexValueInModuleScope_ShouldInlineTheManagementGroupName_2()
{
var result = CompilationHelper.Compile(("main.bicep", @"
targetScope = 'tenant'
// variables
var managementGroups = [
'mg1'
'mg2'
]
resource mgs 'Microsoft.Management/managementGroups@2020-02-01' existing = [for mgname in managementGroups: {
name: mgname
}]
module mod 'mod.bicep' = {
name: 'mod'
scope: mgs[0]
params: {
environment: 'uat'
}
}
"), ("mod.bicep", @"
targetScope = 'managementGroup'
param environment string
output env string = environment
"));

result.Should().NotHaveAnyDiagnostics();
using (new AssertionScope())
{
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'mod')].scope", "[format('Microsoft.Management/managementGroups/{0}', variables('managementGroups')[0])]");
}
}

[TestMethod]
public void UsingIndexValueInModuleScope_ShouldInlineTheResourceGroupAndSubscription()
{
var result = CompilationHelper.Compile(("main.bicep", @"
targetScope = 'subscription'
// variables
var resourceGroups = [
{
name: 'test-uat-rg'
sub: '00000000-0000-0000-0000-000000000000'
}
]
// base resource group deployment
resource rgs 'Microsoft.Resources/resourceGroups@2021-04-01' existing = [for rg in resourceGroups: {
name: rg.name
scope: subscription(rg.sub)
}]
module storage 'storage.bicep' = {
name: 'str'
scope: rgs[0]
params: {
environment: 'uat'
}
}
"), ("storage.bicep", @"
param environment string
output env string = environment
"));

result.Should().NotHaveAnyDiagnostics();
using (new AssertionScope())
{
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'str')].resourceGroup", "[variables('resourceGroups')[0].name]");
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'str')].subscriptionId", "[variables('resourceGroups')[0].sub]");
}
}
[TestMethod]
public void ScopeToNestedChildResource_ShouldGenerateScopeIdCorrectly()
{
var result = CompilationHelper.Compile(("main.bicep", @"
param postgreSqlServerId string
var PSQL_DATABASES = [
{
database: {
name: 'db1'
}
}
{
database: {
name: 'db2'
}
}
]
resource postgreSQL 'Microsoft.DBForPostgreSQL/servers@2017-12-01' existing = {
name: last(split(postgreSqlServerId, '/'))
resource database 'databases' = [for (item, index) in PSQL_DATABASES: {
name: item.database.name
properties: {
charset: contains(item.database, 'charset') ? item.database.charset : 'utf8'
collation: contains(item.database, 'collation') ? item.database.charset : 'English_United States.1252'
}
}]
}
resource dbLocks 'Microsoft.Authorization/locks@2016-09-01' = [for (item, index) in PSQL_DATABASES: {
name: 'dbLock'
scope: postgreSQL::database[index]
properties: {
level: 'CanNotDelete'
notes: 'Database cannot be deleted'
}
}]
"));

result.Should().NotHaveAnyDiagnostics();
using (new AssertionScope())
{
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'dbLock')].scope", "[format('Microsoft.DBforPostgreSQL/servers/{0}/databases/{1}', last(split(parameters('postgreSqlServerId'), '/')), variables('PSQL_DATABASES')[copyIndex()].database.name)]");
result.Template.Should().HaveValueAtPath("$.resources[?(@.name == 'dbLock')].dependsOn", new JArray
{
"[resourceId('Microsoft.DBforPostgreSQL/servers/databases', last(split(parameters('postgreSqlServerId'), '/')), variables('PSQL_DATABASES')[copyIndex()].database.name)]"
});
}
}
}
}
17 changes: 15 additions & 2 deletions src/Bicep.Core/Emit/ExpressionEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ public void EmitExpression(SyntaxBase syntax)
}
}

public void EmitExpression(SyntaxBase resourceNameSyntax, SyntaxBase? indexExpression, SyntaxBase newContext)
{
var converterForContext = converter.CreateConverterForIndexReplacement(resourceNameSyntax, indexExpression, newContext);

var expression = converterForContext.ConvertExpression(resourceNameSyntax);
var serialized = ExpressionSerializer.SerializeExpression(expression);

writer.WriteValue(serialized);
}

public void EmitUnqualifiedResourceId(ResourceMetadata resource, SyntaxBase? indexExpression, SyntaxBase newContext)
{
var converterForContext = converter.CreateConverterForIndexReplacement(resource.NameSyntax, indexExpression, newContext);
Expand Down Expand Up @@ -142,8 +152,11 @@ public LanguageExpression GetFullyQualifiedResourceName(ResourceMetadata resourc
return converter.GetFullyQualifiedResourceName(resource);
}

public LanguageExpression GetManagementGroupResourceId(SyntaxBase managementGroupNameProperty, bool fullyQualified)
=> converter.GenerateManagementGroupResourceId(managementGroupNameProperty, fullyQualified);
public LanguageExpression GetManagementGroupResourceId(SyntaxBase managementGroupNameProperty, SyntaxBase? indexExpression, SyntaxBase newContext, bool fullyQualified)
{
var converterForContext = converter.CreateConverterForIndexReplacement(managementGroupNameProperty, indexExpression, newContext);
return converterForContext.GenerateManagementGroupResourceId(managementGroupNameProperty, fullyQualified);
}

public void EmitLanguageExpression(SyntaxBase syntax)
{
Expand Down
Loading

0 comments on commit 1e9e336

Please sign in to comment.