Skip to content

Commit

Permalink
Fixes deployments with more than one module at tenant scope #3167 (#3176
Browse files Browse the repository at this point in the history
)
  • Loading branch information
BernieWhite authored Nov 11, 2024
1 parent 267fb26 commit 2a086b9
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ What's changed since pre-release v1.40.0-B0103:
- Bug fixes:
- Fixed object to hashtable conversion for default parameter values by @BernieWhite.
[#3033](https://github.com/Azure/PSRule.Rules.Azure/issues/3033)
- Fixed deployments with more than one module at tenant scope by @BernieWhite.
[#3167](https://github.com/Azure/PSRule.Rules.Azure/issues/3167)

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

Expand Down
13 changes: 11 additions & 2 deletions src/PSRule.Rules.Azure/Common/ResourceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal static class ResourceHelper
private const string MANAGEMENT_GROUPS = "managementGroups";
private const string PROVIDERS = "providers";
private const string MANAGEMENT_GROUP_TYPE = "/providers/Microsoft.Management/managementGroups/";
private const string PROVIDER_MICROSOFT_MANAGEMENT = "Microsoft.Management";

private const char SLASH_C = '/';

Expand Down Expand Up @@ -319,7 +320,7 @@ internal static string ResourceId(string? scopeTenant, string? scopeManagementGr
result[i++] = SLASH;
result[i++] = PROVIDERS;
result[i++] = SLASH;
result[i++] = "Microsoft.Management";
result[i++] = PROVIDER_MICROSOFT_MANAGEMENT;
result[i++] = SLASH;
result[i++] = MANAGEMENT_GROUPS;
result[i++] = SLASH;
Expand Down Expand Up @@ -648,12 +649,20 @@ private static bool TryConsumeTenantPart(string[] idParts, ref int start, out st
private static bool TryConsumeManagementGroupPart(string[] idParts, ref int start, out string? managementGroup)
{
managementGroup = null;
if (start == 0 && idParts.Length >= 5 && idParts[0] == string.Empty && StringComparer.OrdinalIgnoreCase.Equals(idParts[1], PROVIDERS) && idParts[2] == "Microsoft.Management" && idParts[3] == MANAGEMENT_GROUPS)
// Handle ID form: /providers/Microsoft.Management/managementGroups/<name>
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)
{
managementGroup = idParts[4];
start += 5;
return true;
}
// Handle scope form: Microsoft.Management/managementGroups/<name>
else if (start == 0 && idParts.Length >= 3 && idParts[0] == PROVIDER_MICROSOFT_MANAGEMENT && idParts[1] == MANAGEMENT_GROUPS)
{
managementGroup = idParts[2];
start += 3;
return true;
}
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/PSRule.Rules.Azure/Data/Template/Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,7 @@ internal static object SubscriptionResourceId(ITemplateContext context, object[]

/// <summary>
/// tenantResourceId(resourceType, resourceName1, [resourceName2], ...)
/// See <see href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-resource#tenantresourceid/" />.
/// </summary>
/// <returns>
/// /providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
Expand Down
19 changes: 18 additions & 1 deletion src/PSRule.Rules.Azure/Data/Template/RuleDataExportVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Management.Automation.Language;
using Newtonsoft.Json.Linq;

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

private static readonly JsonMergeSettings _MergeSettings = new()
{
Expand Down Expand Up @@ -138,6 +138,7 @@ private static void ProjectRuntimeProperties(TemplateContext context, IResourceV
ProjectStorageObjectReplicationPolicies(context, resource) ||
ProjectKeyVault(context, resource) ||
ProjectRoleAssignments(context, resource) ||
ProjectManagementGroup(context, resource) ||
ProjectResource(context, resource);
}

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

private static bool ProjectManagementGroup(TemplateContext context, IResourceValue resource)
{
if (!resource.IsType(TYPE_MANAGEMENT_GROUPS))
return false;

resource.Value.UseProperty(PROPERTY_PROPERTIES, out JObject properties);

// Add properties.tenantId
if (!properties.ContainsKeyInsensitive(PROPERTY_TENANT_ID))
{
properties[PROPERTY_TENANT_ID] = context.Tenant.TenantId;
}

return true;
}

private static bool ProjectRoleAssignments(TemplateContext context, IResourceValue resource)
{
if (!resource.IsType(TYPE_AUTHORIZATION_ROLE_ASSIGNMENTS))
Expand Down
2 changes: 1 addition & 1 deletion src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ private string GetDeploymentScope(string schema, out DeploymentScope deploymentS
if (string.Equals(template, "tenantDeploymentTemplate.json", StringComparison.OrdinalIgnoreCase))
{
deploymentScope = DeploymentScope.Tenant;
return Tenant.Id;
return "/";
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Linq;
using Newtonsoft.Json.Linq;

namespace PSRule.Rules.Azure.Bicep.ScopeTestCases;

/// <summary>
/// Tests for validating resource scopes and IDs are generated correctly.
/// </summary>
public sealed class BicepScopeTests : TemplateVisitorTestsBase
{
[Fact]
public void ProcessTemplate_WhenManagementGroupAtTenant_ShouldReturnCompleteProperties()
{
var resources = ProcessTemplate(GetSourcePath("Bicep/ScopeTestCases/Tests.Bicep.1.json"), null, out _);

Assert.NotNull(resources);

var actual = resources.Where(r => r["name"].Value<string>() == "mg-01").FirstOrDefault();
Assert.Equal("Microsoft.Management/managementGroups", actual["type"].Value<string>());
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-01", actual["id"].Value<string>());
Assert.Equal("/", actual["scope"].Value<string>());
Assert.Equal("mg-01", actual["properties"]["displayName"].Value<string>());
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());

actual = resources.Where(r => r["name"].Value<string>() == "mg-02").FirstOrDefault();
Assert.Equal("Microsoft.Management/managementGroups", actual["type"].Value<string>());
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-02", actual["id"].Value<string>());
Assert.Equal("/", actual["scope"].Value<string>());
Assert.Equal("mg-02", actual["properties"]["displayName"].Value<string>());
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual["properties"]["tenantId"].Value<string>());
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-01", actual["properties"]["details"]["parent"]["id"].Value<string>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

targetScope = 'tenant'

resource mg_2 'Microsoft.Management/managementGroups@2023-04-01' = {
name: 'mg-02'
properties: {
displayName: 'mg-02'
details: {
parent: mg_1
}
}
}

resource mg_1 'Microsoft.Management/managementGroups@2023-04-01' = {
name: 'mg-01'
properties: {
displayName: 'mg-01'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.31.34.60546",
"templateHash": "7600444971325533016"
}
},
"resources": [
{
"type": "Microsoft.Management/managementGroups",
"apiVersion": "2023-04-01",
"name": "mg-02",
"properties": {
"displayName": "mg-02",
"details": {
"parent": "[reference(tenantResourceId('Microsoft.Management/managementGroups', 'mg-01'), '2023-04-01', 'full')]"
}
},
"dependsOn": [
"[tenantResourceId('Microsoft.Management/managementGroups', 'mg-01')]"
]
},
{
"type": "Microsoft.Management/managementGroups",
"apiVersion": "2023-04-01",
"name": "mg-01",
"properties": {
"displayName": "mg-01"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@
<None Update="Bicep\SymbolicNameTestCases\Tests.Bicep.3.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Bicep\ScopeTestCases\Tests.Bicep.1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions tests/PSRule.Rules.Azure.Tests/ResourceHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ public void ResourceId(string resourceType, string resourceName, string scopeId,
[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" })]
[InlineData("Microsoft.Network/virtualNetworks/vnet-A", null, null, null, null, new string[] { "Microsoft.Network/virtualNetworks" }, new string[] { "vnet-A" })]
[InlineData("Microsoft.Network/virtualNetworks/vnet-A/subnets/GatewaySubnet", null, null, null, null, new string[] { "Microsoft.Network/virtualNetworks", "subnets" }, new string[] { "vnet-A", "GatewaySubnet" })]
[InlineData("Microsoft.Management/managementGroups/mg-1", null, "mg-1", null, null, null, null)]
public void ResourceIdComponents(string id, string? tenant, string? managementGroup, string? subscriptionId, string? resourceGroup, string[]? resourceType, string[]? resourceName)
{
Assert.True(ResourceHelper.ResourceIdComponents(id, out var actualTenant, out var actualManagementGroup, out var actualSubscriptionId, out var actualResourceGroup, out var actualResourceType, out var actualResourceName));
Expand Down
1 change: 0 additions & 1 deletion tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data.Template;
using PSRule.Rules.Azure.Pipeline;
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;

namespace PSRule.Rules.Azure
Expand Down

0 comments on commit 2a086b9

Please sign in to comment.