Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes deployments with more than one module at tenant scope #3167 #3176

Merged
Merged
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
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 @@
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 @@ -70,7 +71,7 @@
/// <returns>Returns the resource type if the Id is valid or <c>null</c> when an invalid resource Id is specified.</returns>
internal static string? GetResourceType(string? resourceId)
{
return string.IsNullOrEmpty(resourceId) ? null : string.Join(SLASH, GetResourceIdTypeParts(resourceId));

Check warning on line 74 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'resourceId' in 'string[] ResourceHelper.GetResourceIdTypeParts(string resourceId)'.
}

/// <summary>
Expand Down Expand Up @@ -167,7 +168,7 @@
while (i < result.Length && j <= depth)
{
// If a resource provider is included prepend /providers.
if (resourceType[j].Contains(DOT))

Check warning on line 171 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Dereference of a possibly null reference.
{
result[i++] = SLASH;
result[i++] = PROVIDERS;
Expand All @@ -176,23 +177,23 @@
result[i++] = SLASH;
result[i++] = resourceType[j];
result[i++] = SLASH;
result[i++] = name[j++];

Check warning on line 180 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Dereference of a possibly null reference.
}
}
return string.Concat(result);
}

internal static string CombineResourceId(string subscriptionId, string resourceGroup, string resourceType, string name, int depth = int.MaxValue, string scope = null)

Check warning on line 186 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Cannot convert null literal to non-nullable reference type.
{
TryResourceIdComponents(resourceType, name, out var typeComponents, out var nameComponents);

// Handle scoped resource IDs.
if (!string.IsNullOrEmpty(scope) && scope != SLASH && TryResourceIdComponents(scope, out subscriptionId, out resourceGroup, out var parentTypeComponents, nameComponents: out var parentNameComponents))

Check warning on line 191 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Converting null literal or possible null value to non-nullable type.
{
typeComponents = MergeResourceNameOrType(parentTypeComponents, typeComponents);
nameComponents = MergeResourceNameOrType(parentNameComponents, nameComponents);
}
return CombineResourceId(subscriptionId, resourceGroup, typeComponents, nameComponents, depth);

Check warning on line 196 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'resourceGroup' in 'string ResourceHelper.CombineResourceId(string subscriptionId, string resourceGroup, string[] resourceType, string[] name, int depth = 2147483647)'.

Check warning on line 196 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'resourceType' in 'string ResourceHelper.CombineResourceId(string subscriptionId, string resourceGroup, string[] resourceType, string[] name, int depth = 2147483647)'.

Check warning on line 196 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'name' in 'string ResourceHelper.CombineResourceId(string subscriptionId, string resourceGroup, string[] resourceType, string[] name, int depth = 2147483647)'.
}

internal static string ResourceId(string resourceType, string resourceName, string? scopeId, int depth = int.MaxValue)
Expand Down Expand Up @@ -319,7 +320,7 @@
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 @@ -478,9 +479,9 @@
while (i < result.Length && j <= depth)
{
result[i++] = SLASH;
result[i++] = resourceType[j];

Check warning on line 482 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Dereference of a possibly null reference.
result[i++] = SLASH;
result[i++] = name[j++];

Check warning on line 484 in src/PSRule.Rules.Azure/Common/ResourceHelper.cs

View workflow job for this annotation

GitHub Actions / Build

Dereference of a possibly null reference.
}
}
return string.Concat(result);
Expand Down Expand Up @@ -648,12 +649,20 @@
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
Loading