Fixed loading of expansion options from non-default options #3033 (#3039)

* Fixed loading of expansion options from non-default options #2523

* Update change log
This commit is contained in:
Bernie White 2024-09-15 18:08:58 +10:00 коммит произвёл GitHub
Родитель af1d30b6e3
Коммит 1d4183d37a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
15 изменённых файлов: 500 добавлений и 19 удалений

Просмотреть файл

@ -31,13 +31,15 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
- New rules:
- Virtual Machine:
- Verify that Multitenant Hosting Rights are used for Windows client VMs by @BenjaminEngeset.
- Verify that multi-tenant Hosting Rights are used for Windows client VMs by @BenjaminEngeset.
[#432](https://github.com/Azure/PSRule.Rules.Azure/issues/432)
- Verify that availability set members are in a backend pool by @BenjaminEngeset.
[#67](https://github.com/Azure/PSRule.Rules.Azure/issues/67)
- Bug fixed:
- Fixed `Azure.AppService.AvailabilityZone` only detects premium by tier property @BenjaminEngeset.
[#3034](https://github.com/Azure/PSRule.Rules.Azure/issues/3034)
- Fixed loading of expansion options from non-default options file @BernieWhite.
[#3033](https://github.com/Azure/PSRule.Rules.Azure/issues/3033)
## v1.39.0-B0055 (pre-release)

Просмотреть файл

@ -4,6 +4,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management.Automation;
namespace PSRule.Rules.Azure
{
@ -30,6 +31,23 @@ namespace PSRule.Rules.Azure
return false;
}
public static bool TryPopHashtable(this IDictionary<string, object> dictionary, string key, out Hashtable value)
{
value = null;
if (dictionary.TryPopValue(key, out var o) && o is Hashtable result)
{
value = result;
return true;
}
if (dictionary.TryPopValue(key, out PSObject pso))
{
value = pso.ToHashtable();
return true;
}
return false;
}
[DebuggerStepThrough]
public static bool TryGetValue<T>(this IDictionary<string, object> dictionary, string key, out T value)
{

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.IO;
using System.Management.Automation;
@ -39,5 +40,15 @@ namespace PSRule.Rules.Azure
}
return false;
}
internal static Hashtable ToHashtable(this PSObject o)
{
var result = new Hashtable();
foreach (var p in o.Properties)
{
result[p.Name] = p.Value;
}
return result;
}
}
}

Просмотреть файл

@ -103,6 +103,18 @@ namespace PSRule.Rules.Azure.Configuration
/// </summary>
[DefaultValue(null)]
public string Name { get; set; }
internal static DeploymentOption FromHashtable(Hashtable hashtable)
{
var option = new DeploymentOption();
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("Name", out string s))
option.Name = s;
}
return option;
}
}
/// <summary>
@ -153,8 +165,8 @@ namespace PSRule.Rules.Azure.Configuration
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("Name", out string svalue))
option.Name = svalue;
if (index.TryPopValue("Name", out string s))
option.Name = s;
}
return option;
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
using System.Collections;
using System.ComponentModel;
using YamlDotNet.Serialization;
@ -92,6 +93,24 @@ namespace PSRule.Rules.Azure.Configuration
/// Additional details of the management group.
/// </summary>
public ManagementGroupDetails Details { get; internal set; }
internal static ManagementGroupProperties FromHashtable(Hashtable properties)
{
var result = new ManagementGroupProperties();
if (properties != null)
{
var index = PSRuleOption.BuildIndex(properties);
if (index.TryPopValue("DisplayName", out string displayName))
result.DisplayName = displayName;
if (index.TryPopValue("TenantId", out string tenantId))
result.TenantId = tenantId;
if (index.TryPopHashtable("Details", out var details))
result.Details = ManagementGroupDetails.FromHashtable(details);
}
return result;
}
}
/// <summary>
@ -133,6 +152,27 @@ namespace PSRule.Rules.Azure.Configuration
/// </summary>
[DefaultValue(null)]
public string Version { get; set; }
internal static ManagementGroupDetails FromHashtable(Hashtable details)
{
var result = new ManagementGroupDetails();
if (details != null)
{
var index = PSRuleOption.BuildIndex(details);
if (index.TryPopHashtable("Parent", out var parent))
result.Parent = ManagementGroupParent.FromHashtable(parent);
if (index.TryPopValue("UpdatedBy", out string updatedBy))
result.UpdatedBy = updatedBy;
if (index.TryPopValue("UpdatedTime", out string updatedTime))
result.UpdatedTime = updatedTime;
if (index.TryPopValue("Version", out string version))
result.Version = version;
}
return result;
}
}
/// <summary>
@ -182,6 +222,21 @@ namespace PSRule.Rules.Azure.Configuration
/// </summary>
[DefaultValue(null)]
public string DisplayName { get; set; }
internal static ManagementGroupParent FromHashtable(Hashtable parent)
{
var result = new ManagementGroupParent();
if (parent != null)
{
var index = PSRuleOption.BuildIndex(parent);
if (index.TryPopValue("Name", out string name))
result.Name = name;
if (index.TryPopValue("DisplayName", out string displayName))
result.DisplayName = displayName;
}
return result;
}
}
/// <inheritdoc/>
@ -279,5 +334,20 @@ namespace PSRule.Rules.Azure.Configuration
/// Additional properties of the management group.
/// </summary>
public ManagementGroupProperties Properties { get; set; }
internal static ManagementGroupOption FromHashtable(Hashtable hashtable)
{
var option = new ManagementGroupOption();
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("Name", out string name))
option.Name = name;
if (index.TryPopHashtable("Properties", out var properties))
option.Properties = ManagementGroupProperties.FromHashtable(properties);
}
return option;
}
}
}

Просмотреть файл

@ -104,6 +104,20 @@ namespace PSRule.Rules.Azure.Configuration
return result;
}
internal static ParameterDefaultsOption FromHashtable(Hashtable hashtable)
{
if (hashtable == null || hashtable.Count == 0)
return null;
var result = new ParameterDefaultsOption();
foreach (DictionaryEntry entry in hashtable)
{
if (entry.Key is string key)
result._Defaults[key] = entry.Value;
}
return result;
}
internal bool TryGetString(string parameterName, out JToken value)
{
value = default;

Просмотреть файл

@ -92,6 +92,18 @@ namespace PSRule.Rules.Azure.Configuration
/// The provisioning state of the resource group.
/// </summary>
public string ProvisioningState { get; }
internal static ResourceGroupProperties FromHashtable(Hashtable properties)
{
var option = new ResourceGroupProperties();
if (properties != null)
{
var index = PSRuleOption.BuildIndex(properties);
if (index.TryPopValue("ProvisioningState", out string s))
option = new ResourceGroupProperties(s);
}
return option;
}
}
/// <inheritdoc/>
@ -240,6 +252,33 @@ namespace PSRule.Rules.Azure.Configuration
/// </summary>
[DefaultValue(null)]
public ResourceGroupProperties Properties { get; set; }
internal static ResourceGroupOption FromHashtable(Hashtable hashtable)
{
var option = new ResourceGroupOption();
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("SubscriptionId", out string s))
option.SubscriptionId = s;
if (index.TryPopValue("Name", out s))
option.Name = s;
if (index.TryPopValue("Location", out s))
option.Location = s;
if (index.TryPopValue("ManagedBy", out s))
option.ManagedBy = s;
if (index.TryPopValue("Tags", out Hashtable tags))
option.Tags = tags;
if (index.TryPopHashtable("Properties", out var properties))
option.Properties = ResourceGroupProperties.FromHashtable(properties);
}
return option;
}
}
/// <summary>

Просмотреть файл

@ -127,6 +127,27 @@ namespace PSRule.Rules.Azure.Configuration
return result;
}
internal static SubscriptionOption FromHashtable(Hashtable hashtable)
{
var option = new SubscriptionOption();
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("SubscriptionId", out string subscriptionId))
option.SubscriptionId = subscriptionId;
if (index.TryPopValue("TenantId", out string tenantId))
option.TenantId = tenantId;
if (index.TryPopValue("DisplayName", out string displayName))
option.DisplayName = displayName;
if (index.TryPopValue("State", out string state))
option.State = state;
}
return option;
}
/// <summary>
/// A unique identifier for the subscription.
/// </summary>
@ -232,17 +253,17 @@ namespace PSRule.Rules.Azure.Configuration
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("SubscriptionId", out string svalue))
option.SubscriptionId = svalue;
if (index.TryPopValue("SubscriptionId", out string s))
option.SubscriptionId = s;
if (index.TryPopValue("TenantId", out svalue))
option.TenantId = svalue;
if (index.TryPopValue("TenantId", out s))
option.TenantId = s;
if (index.TryPopValue("DisplayName", out svalue))
option.DisplayName = svalue;
if (index.TryPopValue("DisplayName", out s))
option.DisplayName = s;
if (index.TryPopValue("State", out svalue))
option.State = svalue;
if (index.TryPopValue("State", out s))
option.State = s;
}
return option;
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
using System.Collections;
using System.ComponentModel;
using YamlDotNet.Serialization;
@ -118,6 +119,24 @@ namespace PSRule.Rules.Azure.Configuration
return result;
}
internal static TenantOption FromHashtable(Hashtable hashtable)
{
var result = new TenantOption();
if (hashtable != null)
{
var index = PSRuleOption.BuildIndex(hashtable);
if (index.TryPopValue("CountryCode", out string countryCode))
result.CountryCode = countryCode;
if (index.TryPopValue("TenantId", out string tenantId))
result.TenantId = tenantId;
if (index.TryPopValue("DisplayName", out string displayName))
result.DisplayName = displayName;
}
return result;
}
/// <summary>
/// A country code identifier for the tenant.
/// </summary>

Просмотреть файл

@ -34,7 +34,7 @@ namespace PSRule.Rules.Azure.Runtime
public static string GetBicepVersion(IService service)
{
var s = service as RuntimeService;
var context = GetContext();
var context = GetContext(s);
var bicep = new BicepHelper(
context,
s
@ -117,7 +117,7 @@ namespace PSRule.Rules.Azure.Runtime
/// </summary>
public static PSObject[] GetResources(IService service, string parameterFile)
{
var context = GetContext();
var context = GetContext(service as RuntimeService);
var linkHelper = new TemplateLinkHelper(context, PSRuleOption.GetWorkingPath(), true);
var link = linkHelper.ProcessParameterFile(parameterFile);
if (link == null)
@ -202,7 +202,7 @@ namespace PSRule.Rules.Azure.Runtime
private static PSObject[] GetBicepResources(RuntimeService service, string templateFile, string parameterFile)
{
var context = GetContext();
var context = GetContext(service);
var bicep = new BicepHelper(
context,
service
@ -212,7 +212,7 @@ namespace PSRule.Rules.Azure.Runtime
private static PSObject[] GetBicepParamResources(RuntimeService service, string parameterFile)
{
var context = GetContext();
var context = GetContext(service);
var bicep = new BicepHelper(
context,
service
@ -220,10 +220,11 @@ namespace PSRule.Rules.Azure.Runtime
return bicep.ProcessParamFile(parameterFile);
}
private static PipelineContext GetContext()
private static PipelineContext GetContext(RuntimeService service)
{
PSCmdlet commandRuntime = null;
var option = PSRuleOption.FromFileOrDefault(PSRuleOption.GetWorkingPath());
var option = service.ToPSRuleOption();
var context = new PipelineContext(option, commandRuntime != null ? new PSPipelineWriter(option, commandRuntime) : null);
return context;
}

Просмотреть файл

@ -1,6 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Management.Automation;
#nullable enable
namespace PSRule.Rules.Azure.Runtime
{
/// <summary>
@ -30,5 +34,37 @@ namespace PSRule.Rules.Azure.Runtime
/// <param name="location">The location to check.</param>
/// <returns>Returns <c>true</c> when the location is allowed. Otherwise <c>false</c> is returned.</returns>
bool IsAllowedLocation(string location);
/// <summary>
/// Configure the Azure deployment context.
/// </summary>
void WithAzureDeployment(PSObject? pso);
/// <summary>
/// Configure the Azure resource group context.
/// </summary>
void WithAzureResourceGroup(PSObject? pso);
/// <summary>
/// Configure the Azure subscription context.
/// </summary>
void WithAzureSubscription(PSObject? pso);
/// <summary>
/// Configure the Azure tenant context.
/// </summary>
void WithAzureTenant(PSObject? pso);
/// <summary>
/// Configure the Azure management group context.
/// </summary>
void WithAzureManagementGroup(PSObject? pso);
/// <summary>
/// Configure the parameter defaults.
/// </summary>
void WithParameterDefaults(PSObject? pso);
}
}
#nullable restore

Просмотреть файл

@ -2,8 +2,12 @@
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Management.Automation;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data.Bicep;
#nullable enable
namespace PSRule.Rules.Azure.Runtime
{
/// <summary>
@ -15,7 +19,13 @@ namespace PSRule.Rules.Azure.Runtime
private const int BICEP_TIMEOUT_MAX = 120;
private bool _Disposed;
private HashSet<string> _AllowedLocations;
private HashSet<string>? _AllowedLocations;
private DeploymentOption? _AzureDeployment;
private ResourceGroupOption? _AzureResourceGroup;
private SubscriptionOption? _AzureSubscription;
private TenantOption? _AzureTenant;
private ManagementGroupOption? _AzureManagementGroup;
private ParameterDefaultsOption? _ParameterDefaults;
/// <summary>
/// Create a runtime service.
@ -37,7 +47,7 @@ namespace PSRule.Rules.Azure.Runtime
public string Minimum { get; }
/// <inheritdoc/>
public BicepHelper.BicepInfo Bicep { get; internal set; }
public BicepHelper.BicepInfo? Bicep { get; internal set; }
/// <inheritdoc/>
public void WithAllowedLocations(string[] locations)
@ -62,6 +72,79 @@ namespace PSRule.Rules.Azure.Runtime
_AllowedLocations.Contains(location);
}
/// <inheritdoc/>
public void WithAzureDeployment(PSObject? pso)
{
if (pso == null)
return;
_AzureDeployment = DeploymentOption.FromHashtable(pso.ToHashtable());
}
/// <inheritdoc/>
public void WithAzureResourceGroup(PSObject? pso)
{
if (pso == null)
return;
_AzureResourceGroup = ResourceGroupOption.FromHashtable(pso.ToHashtable());
}
/// <inheritdoc/>
public void WithAzureSubscription(PSObject? pso)
{
if (pso == null)
return;
_AzureSubscription = SubscriptionOption.FromHashtable(pso.ToHashtable());
}
/// <inheritdoc/>
public void WithAzureTenant(PSObject? pso)
{
if (pso == null)
return;
_AzureTenant = TenantOption.FromHashtable(pso.ToHashtable());
}
/// <inheritdoc/>
public void WithAzureManagementGroup(PSObject? pso)
{
if (pso == null)
return;
_AzureManagementGroup = ManagementGroupOption.FromHashtable(pso.ToHashtable());
}
/// <inheritdoc/>
public void WithParameterDefaults(PSObject? pso)
{
if (pso == null)
return;
_ParameterDefaults = ParameterDefaultsOption.FromHashtable(pso.ToHashtable());
}
/// <summary>
/// Get options for the current runtime state.
/// </summary>
internal PSRuleOption ToPSRuleOption()
{
var option = PSRuleOption.FromFileOrDefault(PSRuleOption.GetWorkingPath());
option.Configuration.Deployment = DeploymentOption.Combine(_AzureDeployment, option.Configuration.Deployment);
option.Configuration.ResourceGroup = ResourceGroupOption.Combine(_AzureResourceGroup, option.Configuration.ResourceGroup);
option.Configuration.Subscription = SubscriptionOption.Combine(_AzureSubscription, option.Configuration.Subscription);
option.Configuration.Tenant = TenantOption.Combine(_AzureTenant, option.Configuration.Tenant);
option.Configuration.ManagementGroup = ManagementGroupOption.Combine(_AzureManagementGroup, option.Configuration.ManagementGroup);
option.Configuration.ParameterDefaults = ParameterDefaultsOption.Combine(_ParameterDefaults, option.Configuration.ParameterDefaults);
option.Configuration.BicepFileExpansionTimeout = Timeout;
option.Configuration.BicepMinimumVersion = Minimum;
return option;
}
#region IDisposable
private void Dispose(bool disposing)
@ -86,3 +169,5 @@ namespace PSRule.Rules.Azure.Runtime
#endregion IDisposable
}
}
#nullable restore

Просмотреть файл

@ -25,12 +25,42 @@ Export-PSRuleConvention 'Azure.Context' -Initialize {
$timeout = $Configuration.GetIntegerOrDefault('AZURE_BICEP_FILE_EXPANSION_TIMEOUT', 5);
$check = $Configuration.GetBoolOrDefault('AZURE_BICEP_CHECK_TOOL', $False);
$allowedRegions = @($Configuration.GetValueOrDefault('Azure_AllowedRegions', $Configuration.GetStringValues('AZURE_RESOURCE_ALLOWED_LOCATIONS')));
$azureDeployment = $Configuration.GetValueOrDefault('AZURE_DEPLOYMENT', $Null);
$azureResourceGroup = $Configuration.GetValueOrDefault('AZURE_RESOURCE_GROUP', $Null);
$azureSubscription = $Configuration.GetValueOrDefault('AZURE_SUBSCRIPTION', $Null);
$azureTenant = $Configuration.GetValueOrDefault('AZURE_TENANT', $Null);
$azureManagementGroup = $Configuration.GetValueOrDefault('AZURE_MANAGEMENT_GROUP', $Null);
$azureParameterDefaults = $Configuration.GetValueOrDefault('AZURE_PARAMETER_DEFAULTS', $Null);
$service = [PSRule.Rules.Azure.Runtime.Helper]::CreateService($minimum, $timeout);
if ($allowedRegions.Length -gt 0) {
$service.WithAllowedLocations($allowedRegions);
}
if ($Null -ne $azureDeployment) {
$service.WithAzureDeployment($azureDeployment);
}
if ($Null -ne $azureResourceGroup) {
$service.WithAzureResourceGroup($azureResourceGroup);
}
if ($Null -ne $azureSubscription) {
$service.WithAzureSubscription($azureSubscription);
}
if ($Null -ne $azureTenant) {
$service.WithAzureTenant($azureTenant);
}
if ($Null -ne $azureManagementGroup) {
$service.WithAzureManagementGroup($azureManagementGroup);
}
if ($Null -ne $azureParameterDefaults) {
$service.WithParameterDefaults($azureParameterDefaults);
}
if ($check) {
Write-Verbose "[Azure.Context] -- Checking Bicep CLI.";
$version = [PSRule.Rules.Azure.Runtime.Helper]::GetBicepVersion($service);

Просмотреть файл

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
global using Xunit;

Просмотреть файл

@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Management.Automation;
using PSRule.Rules.Azure.Runtime;
namespace PSRule.Rules.Azure;
public sealed class RuntimeServiceTests
{
[Fact]
public void ToPSRuleOption_WhenNotSet_ShouldUseDefaults()
{
// Arrange
var runtime = GetRuntimeService();
// Act
var actual = runtime.ToPSRuleOption();
// Assert
Assert.NotNull(actual.Configuration);
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual.Configuration.Subscription.SubscriptionId);
Assert.Equal("ps-rule-test-rg", actual.Configuration.ResourceGroup.Name);
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual.Configuration.Tenant.TenantId);
Assert.Equal("ps-rule-test-deployment", actual.Configuration.Deployment.Name);
Assert.Equal("Azure", actual.Configuration.PolicyRulePrefix);
Assert.Equal("0.4.451", actual.Configuration.BicepMinimumVersion);
Assert.Equal(5, actual.Configuration.BicepFileExpansionTimeout);
}
[Fact]
public void WithAzureSubscription_WhenValid_ShouldSetOptions()
{
var runtime = GetRuntimeService();
var pso = new PSObject();
pso.Properties.Add(new PSNoteProperty("subscriptionId", "value1"));
// Act
runtime.WithAzureSubscription(pso);
// Assert
var actual = runtime.ToPSRuleOption();
Assert.Equal("value1", actual.Configuration.Subscription.SubscriptionId);
}
[Fact]
public void WithAzureResourceGroup_WhenValid_ShouldSetOptions()
{
var runtime = GetRuntimeService();
var pso = new PSObject();
pso.Properties.Add(new PSNoteProperty("name", "value1"));
// Act
runtime.WithAzureResourceGroup(pso);
// Assert
var actual = runtime.ToPSRuleOption();
Assert.Equal("value1", actual.Configuration.ResourceGroup.Name);
}
[Fact]
public void WithAzureTenant_WhenValid_ShouldSetOptions()
{
var runtime = GetRuntimeService();
var pso = new PSObject();
pso.Properties.Add(new PSNoteProperty("tenantId", "value1"));
// Act
runtime.WithAzureTenant(pso);
// Assert
var actual = runtime.ToPSRuleOption();
Assert.Equal("value1", actual.Configuration.Tenant.TenantId);
}
[Fact]
public void WithAzureDeployment_WhenValid_ShouldSetOptions()
{
var runtime = GetRuntimeService();
var pso = new PSObject();
pso.Properties.Add(new PSNoteProperty("name", "value1"));
// Act
runtime.WithAzureDeployment(pso);
// Assert
var actual = runtime.ToPSRuleOption();
Assert.Equal("value1", actual.Configuration.Deployment.Name);
}
[Fact]
public void WithParameterDefaults_WhenValid_ShouldSetOptions()
{
var runtime = GetRuntimeService();
var pso = new PSObject();
pso.Properties.Add(new PSNoteProperty("value1", "1"));
pso.Properties.Add(new PSNoteProperty("value2", "2"));
// Act
runtime.WithParameterDefaults(pso);
// Assert
var actual = runtime.ToPSRuleOption();
Assert.True(actual.Configuration.ParameterDefaults.TryGetString("value1", out string value1));
Assert.Equal("1", value1);
Assert.True(actual.Configuration.ParameterDefaults.TryGetString("value2", out string value2));
Assert.Equal("2", value2);
}
#region Helper methods
private static RuntimeService GetRuntimeService()
{
// Arrange
return new RuntimeService("0.4.451", 5);
}
#endregion Helper methods
}