Skip to content

Commit 2a086b9

Browse files
authoredNov 11, 2024··
Fixes deployments with more than one module at tenant scope Azure#3167 (Azure#3176)
1 parent 267fb26 commit 2a086b9

11 files changed

+129
-5
lines changed
 

‎docs/CHANGELOG-v1.md

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ What's changed since pre-release v1.40.0-B0103:
3434
- Bug fixes:
3535
- Fixed object to hashtable conversion for default parameter values by @BernieWhite.
3636
[#3033](https://github.com/Azure/PSRule.Rules.Azure/issues/3033)
37+
- Fixed deployments with more than one module at tenant scope by @BernieWhite.
38+
[#3167](https://github.com/Azure/PSRule.Rules.Azure/issues/3167)
3739

3840
## v1.40.0-B0103 (pre-release)
3941

‎src/PSRule.Rules.Azure/Common/ResourceHelper.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal static class ResourceHelper
1818
private const string MANAGEMENT_GROUPS = "managementGroups";
1919
private const string PROVIDERS = "providers";
2020
private const string MANAGEMENT_GROUP_TYPE = "/providers/Microsoft.Management/managementGroups/";
21+
private const string PROVIDER_MICROSOFT_MANAGEMENT = "Microsoft.Management";
2122

2223
private const char SLASH_C = '/';
2324

@@ -319,7 +320,7 @@ internal static string ResourceId(string? scopeTenant, string? scopeManagementGr
319320
result[i++] = SLASH;
320321
result[i++] = PROVIDERS;
321322
result[i++] = SLASH;
322-
result[i++] = "Microsoft.Management";
323+
result[i++] = PROVIDER_MICROSOFT_MANAGEMENT;
323324
result[i++] = SLASH;
324325
result[i++] = MANAGEMENT_GROUPS;
325326
result[i++] = SLASH;
@@ -648,12 +649,20 @@ private static bool TryConsumeTenantPart(string[] idParts, ref int start, out st
648649
private static bool TryConsumeManagementGroupPart(string[] idParts, ref int start, out string? managementGroup)
649650
{
650651
managementGroup = null;
651-
if (start == 0 && idParts.Length >= 5 && idParts[0] == string.Empty && StringComparer.OrdinalIgnoreCase.Equals(idParts[1], PROVIDERS) && idParts[2] == "Microsoft.Management" && idParts[3] == MANAGEMENT_GROUPS)
652+
// Handle ID form: /providers/Microsoft.Management/managementGroups/<name>
653+
if (start == 0 && idParts.Length >= 5 && idParts[0] == string.Empty && StringComparer.OrdinalIgnoreCase.Equals(idParts[1], PROVIDERS) && idParts[2] == PROVIDER_MICROSOFT_MANAGEMENT && idParts[3] == MANAGEMENT_GROUPS)
652654
{
653655
managementGroup = idParts[4];
654656
start += 5;
655657
return true;
656658
}
659+
// Handle scope form: Microsoft.Management/managementGroups/<name>
660+
else if (start == 0 && idParts.Length >= 3 && idParts[0] == PROVIDER_MICROSOFT_MANAGEMENT && idParts[1] == MANAGEMENT_GROUPS)
661+
{
662+
managementGroup = idParts[2];
663+
start += 3;
664+
return true;
665+
}
657666
return false;
658667
}
659668

‎src/PSRule.Rules.Azure/Data/Template/Functions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,7 @@ internal static object SubscriptionResourceId(ITemplateContext context, object[]
11271127

11281128
/// <summary>
11291129
/// tenantResourceId(resourceType, resourceName1, [resourceName2], ...)
1130+
/// See <see href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-resource#tenantresourceid/" />.
11301131
/// </summary>
11311132
/// <returns>
11321133
/// /providers/{resourceProviderNamespace}/{resourceType}/{resourceName}

‎src/PSRule.Rules.Azure/Data/Template/RuleDataExportVisitor.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Management.Automation.Language;
76
using Newtonsoft.Json.Linq;
87

98
namespace PSRule.Rules.Azure.Data.Template;
@@ -58,6 +57,7 @@ internal sealed class RuleDataExportVisitor : TemplateVisitor
5857
private const string TYPE_KEYVAULT = "Microsoft.KeyVault/vaults";
5958
private const string TYPE_STORAGE_OBJECTREPLICATIONPOLICIES = "Microsoft.Storage/storageAccounts/objectReplicationPolicies";
6059
private const string TYPE_AUTHORIZATION_ROLE_ASSIGNMENTS = "Microsoft.Authorization/roleAssignments";
60+
private const string TYPE_MANAGEMENT_GROUPS = "Microsoft.Management/managementGroups";
6161

6262
private static readonly JsonMergeSettings _MergeSettings = new()
6363
{
@@ -138,6 +138,7 @@ private static void ProjectRuntimeProperties(TemplateContext context, IResourceV
138138
ProjectStorageObjectReplicationPolicies(context, resource) ||
139139
ProjectKeyVault(context, resource) ||
140140
ProjectRoleAssignments(context, resource) ||
141+
ProjectManagementGroup(context, resource) ||
141142
ProjectResource(context, resource);
142143
}
143144

@@ -157,6 +158,22 @@ private static bool ProjectResource(TemplateContext context, IResourceValue reso
157158
return true;
158159
}
159160

161+
private static bool ProjectManagementGroup(TemplateContext context, IResourceValue resource)
162+
{
163+
if (!resource.IsType(TYPE_MANAGEMENT_GROUPS))
164+
return false;
165+
166+
resource.Value.UseProperty(PROPERTY_PROPERTIES, out JObject properties);
167+
168+
// Add properties.tenantId
169+
if (!properties.ContainsKeyInsensitive(PROPERTY_TENANT_ID))
170+
{
171+
properties[PROPERTY_TENANT_ID] = context.Tenant.TenantId;
172+
}
173+
174+
return true;
175+
}
176+
160177
private static bool ProjectRoleAssignments(TemplateContext context, IResourceValue resource)
161178
{
162179
if (!resource.IsType(TYPE_AUTHORIZATION_ROLE_ASSIGNMENTS))

‎src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ private string GetDeploymentScope(string schema, out DeploymentScope deploymentS
549549
if (string.Equals(template, "tenantDeploymentTemplate.json", StringComparison.OrdinalIgnoreCase))
550550
{
551551
deploymentScope = DeploymentScope.Tenant;
552-
return Tenant.Id;
552+
return "/";
553553
}
554554
}
555555

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Linq;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace PSRule.Rules.Azure.Bicep.ScopeTestCases;
8+
9+
/// <summary>
10+
/// Tests for validating resource scopes and IDs are generated correctly.
11+
/// </summary>
12+
public sealed class BicepScopeTests : TemplateVisitorTestsBase
13+
{
14+
[Fact]
15+
public void ProcessTemplate_WhenManagementGroupAtTenant_ShouldReturnCompleteProperties()
16+
{
17+
var resources = ProcessTemplate(GetSourcePath("Bicep/ScopeTestCases/Tests.Bicep.1.json"), null, out _);
18+
19+
Assert.NotNull(resources);
20+
21+
var actual = resources.Where(r => r["name"].Value<string>() == "mg-01").FirstOrDefault();
22+
Assert.Equal("Microsoft.Management/managementGroups", actual["type"].Value<string>());
23+
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-01", actual["id"].Value<string>());
24+
Assert.Equal("/", actual["scope"].Value<string>());
25+
Assert.Equal("mg-01", actual["properties"]["displayName"].Value<string>());
26+
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());
27+
28+
actual = resources.Where(r => r["name"].Value<string>() == "mg-02").FirstOrDefault();
29+
Assert.Equal("Microsoft.Management/managementGroups", actual["type"].Value<string>());
30+
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-02", actual["id"].Value<string>());
31+
Assert.Equal("/", actual["scope"].Value<string>());
32+
Assert.Equal("mg-02", actual["properties"]["displayName"].Value<string>());
33+
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());
34+
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-01", actual["properties"]["details"]["parent"]["id"].Value<string>());
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
targetScope = 'tenant'
5+
6+
resource mg_2 'Microsoft.Management/managementGroups@2023-04-01' = {
7+
name: 'mg-02'
8+
properties: {
9+
displayName: 'mg-02'
10+
details: {
11+
parent: mg_1
12+
}
13+
}
14+
}
15+
16+
resource mg_1 'Microsoft.Management/managementGroups@2023-04-01' = {
17+
name: 'mg-01'
18+
properties: {
19+
displayName: 'mg-01'
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
3+
"contentVersion": "1.0.0.0",
4+
"metadata": {
5+
"_generator": {
6+
"name": "bicep",
7+
"version": "0.31.34.60546",
8+
"templateHash": "7600444971325533016"
9+
}
10+
},
11+
"resources": [
12+
{
13+
"type": "Microsoft.Management/managementGroups",
14+
"apiVersion": "2023-04-01",
15+
"name": "mg-02",
16+
"properties": {
17+
"displayName": "mg-02",
18+
"details": {
19+
"parent": "[reference(tenantResourceId('Microsoft.Management/managementGroups', 'mg-01'), '2023-04-01', 'full')]"
20+
}
21+
},
22+
"dependsOn": [
23+
"[tenantResourceId('Microsoft.Management/managementGroups', 'mg-01')]"
24+
]
25+
},
26+
{
27+
"type": "Microsoft.Management/managementGroups",
28+
"apiVersion": "2023-04-01",
29+
"name": "mg-01",
30+
"properties": {
31+
"displayName": "mg-01"
32+
}
33+
}
34+
]
35+
}

‎tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@
314314
<None Update="Bicep\SymbolicNameTestCases\Tests.Bicep.3.json">
315315
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
316316
</None>
317+
<None Update="Bicep\ScopeTestCases\Tests.Bicep.1.json">
318+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
319+
</None>
317320
</ItemGroup>
318321

319322
</Project>

‎tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public void ResourceId(string resourceType, string resourceName, string scopeId,
151151
[InlineData("/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/resourceGroups/rg-5/providers/Microsoft.KeyVault/vaults/keyvault-1/secrets/secret-1", null, null, "ffffffff-ffff-ffff-ffff-ffffffffffff", "rg-5", new string[] { "Microsoft.KeyVault/vaults", "secrets" }, new string[] { "keyvault-1", "secret-1" })]
152152
[InlineData("Microsoft.Network/virtualNetworks/vnet-A", null, null, null, null, new string[] { "Microsoft.Network/virtualNetworks" }, new string[] { "vnet-A" })]
153153
[InlineData("Microsoft.Network/virtualNetworks/vnet-A/subnets/GatewaySubnet", null, null, null, null, new string[] { "Microsoft.Network/virtualNetworks", "subnets" }, new string[] { "vnet-A", "GatewaySubnet" })]
154+
[InlineData("Microsoft.Management/managementGroups/mg-1", null, "mg-1", null, null, null, null)]
154155
public void ResourceIdComponents(string id, string? tenant, string? managementGroup, string? subscriptionId, string? resourceGroup, string[]? resourceType, string[]? resourceName)
155156
{
156157
Assert.True(ResourceHelper.ResourceIdComponents(id, out var actualTenant, out var actualManagementGroup, out var actualSubscriptionId, out var actualResourceGroup, out var actualResourceType, out var actualResourceName));

‎tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Newtonsoft.Json.Linq;
66
using PSRule.Rules.Azure.Configuration;
77
using PSRule.Rules.Azure.Data.Template;
8-
using PSRule.Rules.Azure.Pipeline;
98
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
109

1110
namespace PSRule.Rules.Azure

0 commit comments

Comments
 (0)
Please sign in to comment.