diff --git a/docs/changelog.md b/docs/changelog.md index 1cf953eb6c8..7a5c63810af 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -41,6 +41,13 @@ What's changed since pre-release v1.48.0-B0228: - Container Registry: - Deprecated `Azure.ACR.GeoReplica` because ACR zone redundancy is automatic in supported regions. [#3846](https://github.com/Azure/PSRule.Rules.Azure/issues/3846) +- Bug fixes: + - Fixed `cidrHost` and `cidrSubnet` failing on unresolved virtual network, subnet, and IPAM pool address prefixes by @oWretch. + [#3907](https://github.com/Azure/PSRule.Rules.Azure/issues/3907) + - Fixed `tryGet` throwing instead of returning `null` for a property lookup against an array by @oWretch. + [#3907](https://github.com/Azure/PSRule.Rules.Azure/issues/3907) + - Fixed cross-scope `existing` resource reference via symbolic name resolving to a malformed mock, breaking `concat()`/`map()` during pre-flight expansion. + [#3920](https://github.com/Azure/PSRule.Rules.Azure/issues/3920) - Engineering: - Bump YamlDotNet to 11.2.5. diff --git a/src/PSRule.Rules.Azure/Arm/Deployments/TemplateContext.cs b/src/PSRule.Rules.Azure/Arm/Deployments/TemplateContext.cs index a2bc1bf340d..53ad12eed3c 100644 --- a/src/PSRule.Rules.Azure/Arm/Deployments/TemplateContext.cs +++ b/src/PSRule.Rules.Azure/Arm/Deployments/TemplateContext.cs @@ -219,12 +219,33 @@ public bool TryGetResource(string nameOrResourceId, out IResourceValue? resource return false; var resourceId = nameOrResourceId; + IResourceValue? symbolResource = null; if (_Symbols.TryGetValue(nameOrResourceId, out var symbol) && symbol != null) - resourceId = symbol.GetId(0); + { + symbol.TryGetResource(0, out symbolResource); + + // The ID of an existing resource is expanded on demand and may not be resolvable. + try + { + resourceId = symbol.GetId(0); + } + catch + { + resourceId = null; + } + } if (resourceId != null && _ResourceIds.TryGetValue(resourceId, out resource)) return true; + // Fall back to the resource attached to the symbol. Existing resources are tracked as symbols + // but are not added as deployable resources, so they are not in the resource ID lookup. + if (symbolResource != null) + { + resource = symbolResource; + return true; + } + // Recurse search for resource in the parent deployment by original resource ID only. if (Parent != null && ResourceHelper.IsResourceId(nameOrResourceId) && Parent.TryGetResource(nameOrResourceId, out resource)) return true; @@ -240,11 +261,27 @@ public bool TryGetResourceCollection(string symbolicName, out IResourceValue[]? return false; var ids = array.GetIds(); - resources = new IResourceValue[ids.Length]; + var byId = new IResourceValue[ids.Length]; + var resolved = true; for (var i = 0; i < ids.Length; i++) - resources[i] = _ResourceIds[ids[i]]; + { + if (ids[i] == null || !_ResourceIds.TryGetValue(ids[i], out var item)) + { + resolved = false; + break; + } + byId[i] = item; + } - return true; + if (resolved) + { + resources = byId; + return true; + } + + // Fall back to the resources attached to the symbol for existing resources. + resources = array.GetResources(); + return resources.Length > 0; } #nullable restore diff --git a/src/PSRule.Rules.Azure/Arm/Expressions/ExpressionHelpers.cs b/src/PSRule.Rules.Azure/Arm/Expressions/ExpressionHelpers.cs index 03445481935..9c496f1a7c0 100644 --- a/src/PSRule.Rules.Azure/Arm/Expressions/ExpressionHelpers.cs +++ b/src/PSRule.Rules.Azure/Arm/Expressions/ExpressionHelpers.cs @@ -236,7 +236,7 @@ internal static bool TryPropertyOrField(object o, string propertyName, out objec return true; } - if (o is JToken jToken && o is not JValue) + if (o is JToken jToken && o is not JValue && o is not JArray) { var propertyToken = jToken[propertyName]; if (propertyToken == null) diff --git a/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs b/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs index 3006703ec38..ada6d510afc 100644 --- a/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs +++ b/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs @@ -289,21 +289,19 @@ internal static object Concat(ITemplateContext context, object[] args) return result.ToString(); } // Array - else if (args[0] is Array || args[0] is JArray) + else if (ExpressionHelpers.TryArray(args[0], out var firstArray)) { var result = new List(); - for (var i = 0; i < args.Length; i++) + for (var j = 0; j < firstArray.Length; j++) + result.Add(firstArray.GetValue(j)); + + for (var i = 1; i < args.Length; i++) { - if (args[i] is Array array) + if (ExpressionHelpers.TryArray(args[i], out var array)) { for (var j = 0; j < array.Length; j++) result.Add(array.GetValue(j)); } - else if (args[i] is JArray jArray) - { - for (var j = 0; j < jArray.Count; j++) - result.Add(jArray[j]); - } } return result.ToArray(); } @@ -1176,10 +1174,13 @@ private static object GetReferenceResult(IResourceValue resource, bool full) return full ? deployment : deployment.Properties; if (resource.Existing && !resource.Value.TryGetProperty(PROPERTY_PROPERTIES, out _)) - return full ? new Mock.MockResource(resource.Id) : new Mock.MockResource(resource.Id)[PROPERTY_PROPERTIES]; + { + var mockResourceId = GetResourceIdOrSymbolicName(resource); + return full ? new Mock.MockResource(mockResourceId, resource.Type) : new Mock.MockResource(mockResourceId, resource.Type)[PROPERTY_PROPERTIES]; + } if (!full && resource.Value.TryGetProperty(PROPERTY_PROPERTIES, out var properties)) - return new Mock.MockObject(properties); + return new Mock.MockResourceObject(properties, GetResourceIdOrSymbolicName(resource), resource.Type); return new Mock.MockObject(full ? resource.Value : new JObject()); } @@ -2736,11 +2737,28 @@ private static bool TryResourceIdOrSymbolicName(ITemplateContext context, string resourceId = resourceIdOrSymbolicName; if (context.TryGetResource(resourceIdOrSymbolicName, out var resource) && resource != null) - resourceId = resource.Id; + resourceId = GetResourceIdOrSymbolicName(resource); return resourceId != null; } + /// + /// Get the resource ID of a resource, falling back to the symbolic name. + /// The ID of an existing resource is expanded on demand and may not be resolvable, for example when + /// the scope of the resource depends on a value that is not known during expansion. + /// + private static string GetResourceIdOrSymbolicName(IResourceValue resource) + { + try + { + return resource.Id; + } + catch + { + return resource.SymbolicName; + } + } + private static int Compare(object left, object right) { if (ExpressionHelpers.TryLong(left, out var longLeft) && ExpressionHelpers.TryLong(right, out var longRight)) diff --git a/src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs b/src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs index 7cc520e9130..78a9c0717b7 100644 --- a/src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs +++ b/src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs @@ -12,6 +12,52 @@ namespace PSRule.Rules.Azure.Arm.Mocks; internal sealed class Mock { + private const string RESOURCE_TYPE_VIRTUAL_NETWORK = "Microsoft.Network/virtualNetworks"; + private const string RESOURCE_TYPE_SUBNET = "Microsoft.Network/virtualNetworks/subnets"; + private const string RESOURCE_TYPE_IPAM_POOL = "Microsoft.Network/networkManagers/ipamPools"; + private const string PATH_VIRTUAL_NETWORK_ADDRESS_PREFIXES = "addressSpace.addressPrefixes"; + private const string PATH_SUBNET_ADDRESS_PREFIX = "addressPrefix"; + private const string PATH_SUBNET_ADDRESS_PREFIXES = "addressPrefixes"; + private const string PLACEHOLDER_VIRTUAL_NETWORK_CIDR = "192.0.2.0/24"; + private const string PLACEHOLDER_SUBNET_CIDR = "192.0.2.0/28"; + + private static readonly MockPlaceholderDescriptor[] _ResourcePlaceholders = + [ + new(RESOURCE_TYPE_VIRTUAL_NETWORK, PATH_VIRTUAL_NETWORK_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_VIRTUAL_NETWORK_CIDR]), + new(RESOURCE_TYPE_SUBNET, PATH_SUBNET_ADDRESS_PREFIX, MockPlaceholderKind.String, [PLACEHOLDER_SUBNET_CIDR]), + new(RESOURCE_TYPE_SUBNET, PATH_SUBNET_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_SUBNET_CIDR]), + new(RESOURCE_TYPE_IPAM_POOL, PATH_SUBNET_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_VIRTUAL_NETWORK_CIDR]), + ]; + + private enum MockPlaceholderKind + { + String, + Array + } + + private sealed class MockPlaceholderDescriptor(string resourceType, string propertyPath, MockPlaceholderKind kind, string[] values) + { + public string ResourceType { get; } = resourceType; + + public string PropertyPath { get; } = propertyPath; + + public MockPlaceholderKind Kind { get; } = kind; + + private string[] Values { get; } = values; + + public bool TryGetValue(bool secret, out MockValue value) + { + value = Values.Length > 0 ? new MockValue(Values[0], secret) : null!; + return Values.Length > 0; + } + + public void AddValues(JArray array, bool secret) + { + for (var i = 0; i < Values.Length; i++) + array.Add(new MockValue(Values[i], secret)); + } + } + /// /// Mock an unknown property or value. /// @@ -79,10 +125,14 @@ public override string ToString() internal sealed class MockResource : MockUnknownObject { public MockResource(string resourceId) + : this(resourceId, resourceType: null) { } + + public MockResource(string resourceId, string? resourceType) : base() { ResourceId = resourceId; - ResourceHelper.TryResourceIdComponents(resourceId, out var subscriptionId, out var resourceGroupName, out string? resourceType, out string? name); + ResourceHelper.TryResourceIdComponents(resourceId, out var subscriptionId, out var resourceGroupName, out string? resourceIdType, out string? name); + ResourceType = resourceType ?? resourceIdType ?? string.Empty; if (resourceId != null) { Add("id", new JValue(resourceId)); @@ -93,9 +143,9 @@ public MockResource(string resourceId) Add("subscriptionId", new JValue(subscriptionId)); } - if (resourceType != null) + if (ResourceType.Length > 0) { - Add("type", new JValue(resourceType)); + Add("type", new JValue(ResourceType)); } if (name != null) @@ -103,12 +153,14 @@ public MockResource(string resourceId) Add("name", new JValue(name)); } - var properties = new MockResourceProperties(resourceId ?? string.Empty); + var properties = new MockResourceProperties(resourceId ?? string.Empty, ResourceType); Add("properties", properties); } public string ResourceId { get; } + public string ResourceType { get; } + public override JToken? this[object key] { get => base[key]; set => base[key] = value; } public override string GetString() @@ -125,58 +177,142 @@ internal interface IMockResourceCollection internal sealed class MockResourceProperties : MockUnknownObject { private readonly string _ResourceId; + private readonly string _ResourceType; - public MockResourceProperties(string resourceId) + public MockResourceProperties(string resourceId, string resourceType) : base() { _ResourceId = resourceId; + _ResourceType = resourceType; } protected override JToken CreateUnknownProperty(object key) { - return key is string propertyName ? new MockResourceProperty(_ResourceId, propertyName, IsSecret) : base.CreateUnknownProperty(key); + return key is string propertyName ? CreateResourceProperty(_ResourceId, _ResourceType, propertyName, propertyName, IsSecret) : base.CreateUnknownProperty(key); + } + } + + /// + /// The properties of a resource that is known, but may only define a subset of its properties. + /// Concrete properties are returned as-is, while known address properties that were not defined in the + /// source resolve to a typed placeholder instead of an unknown value. + /// + internal sealed class MockResourceObject : MockObject + { + private readonly string _ResourceId; + private readonly string _ResourceType; + + public MockResourceObject(JObject value, string resourceId, string resourceType) + : base(value) + { + _ResourceId = resourceId; + _ResourceType = resourceType; + } + + protected override JToken CreateUnknownProperty(object key) + { + return key is string propertyName ? CreateResourceProperty(_ResourceId, _ResourceType, propertyName, propertyName, IsSecret) : base.CreateUnknownProperty(key); } } internal sealed class MockResourceProperty : MockUnknownObject, IMockResourceCollection { private readonly string _ResourceId; + private readonly string _ResourceType; private readonly string _PropertyName; + private readonly string _PropertyPath; private MockResourcePropertyArray? _Array; - public MockResourceProperty(string resourceId, string propertyName, bool secret) + public MockResourceProperty(string resourceId, string resourceType, string propertyName, bool secret) + : this(resourceId, resourceType, propertyName, propertyName, secret) { } + + internal MockResourceProperty(string resourceId, string resourceType, string propertyName, string propertyPath, bool secret) : base(secret) { _ResourceId = resourceId; + _ResourceType = resourceType; _PropertyName = propertyName; + _PropertyPath = propertyPath; } public override JToken? GetValue(TypePrimitive type) { + if (type == TypePrimitive.Array && TryGetPlaceholder(out var descriptor) && descriptor.Kind == MockPlaceholderKind.Array) + return ToArray(); + return type == TypePrimitive.Array ? ToArray() : base.GetValue(type); } + public override JToken? GetValue(object key) + { + key = GetBaseObject(key); + if (TryGetPlaceholder(out var descriptor)) + { + if (descriptor.Kind == MockPlaceholderKind.Array && key is int) + return ToArray().GetValue(key); + } + return base.GetValue(key); + } + + public override TValue? GetValue() where TValue : default + { + if (typeof(TValue) == typeof(string) && + TryGetPlaceholder(out var descriptor) && + descriptor.Kind == MockPlaceholderKind.String && + descriptor.TryGetValue(IsSecret, out var value) && + value.Value() is TValue result) + return result; + + return base.GetValue(); + } + private MockResourcePropertyArray ToArray() { - return _Array ??= new MockResourcePropertyArray(_ResourceId, _PropertyName, IsSecret); + return _Array ??= new MockResourcePropertyArray(_ResourceId, _ResourceType, _PropertyName, _PropertyPath, IsSecret); } public MockResourcePropertyItem CreateItem() { return new MockResourcePropertyItem(_ResourceId, _PropertyName, IsSecret); } + + protected override JToken CreateUnknownProperty(object key) + { + return key is string propertyName ? CreateResourceProperty(_ResourceId, _ResourceType, propertyName, string.Concat(_PropertyPath, ".", propertyName), IsSecret) : base.CreateUnknownProperty(key); + } + + private bool TryGetPlaceholder(out MockPlaceholderDescriptor descriptor) + { + return TryResourcePlaceholder(_ResourceType, _PropertyPath, out descriptor); + } } internal sealed class MockResourcePropertyArray : MockArray, IMockResourceCollection { private readonly string _ResourceId; + private readonly string _ResourceType; private readonly string _PropertyName; + private readonly string _PropertyPath; - public MockResourcePropertyArray(string resourceId, string propertyName, bool secret) + public MockResourcePropertyArray(string resourceId, string resourceType, string propertyName, string propertyPath, bool secret) : base(secret) { _ResourceId = resourceId; + _ResourceType = resourceType; _PropertyName = propertyName; + _PropertyPath = propertyPath; + + if (TryResourcePlaceholder(_ResourceType, _PropertyPath, out var descriptor) && descriptor.Kind == MockPlaceholderKind.Array) + descriptor.AddValues(this, secret); + } + + public override JToken? GetValue(object key) + { + key = GetBaseObject(key); + if (key is int && TryResourcePlaceholder(_ResourceType, _PropertyPath, out var descriptor) && descriptor.TryGetValue(IsSecret, out var value)) + return value; + + return base.GetValue(key); } public MockResourcePropertyItem CreateItem() @@ -359,7 +495,7 @@ public MockArray(bool secret = false) return type == TypePrimitive.None || type == TypePrimitive.Array ? this : null; } - public JToken? GetValue(object key) + public virtual JToken? GetValue(object key) { if (key is long l) key = (int)l; @@ -638,6 +774,35 @@ private static TypePrimitive GetTypePrimitive(JToken token) throw new NotImplementedException(); } + private static JToken CreateResourceProperty(string resourceId, string resourceType, string propertyName, string propertyPath, bool secret) + { + if (TryResourcePlaceholder(resourceType, propertyPath, out var descriptor)) + { + if (descriptor.Kind == MockPlaceholderKind.Array) + return new MockResourcePropertyArray(resourceId, resourceType, propertyName, propertyPath, secret); + + if (descriptor.TryGetValue(secret, out var value)) + return value; + } + return new MockResourceProperty(resourceId, resourceType, propertyName, propertyPath, secret); + } + + private static bool TryResourcePlaceholder(string resourceType, string propertyPath, out MockPlaceholderDescriptor descriptor) + { + descriptor = null!; + for (var i = 0; i < _ResourcePlaceholders.Length; i++) + { + var placeholder = _ResourcePlaceholders[i]; + if (StringComparer.OrdinalIgnoreCase.Equals(resourceType, placeholder.ResourceType) && + StringComparer.OrdinalIgnoreCase.Equals(propertyPath, placeholder.PropertyPath)) + { + descriptor = placeholder; + return true; + } + } + return false; + } + private static bool TryWellKnownStringProperty(JObject o, string key, out JValue? value) { value = default; diff --git a/src/PSRule.Rules.Azure/Arm/Symbols/ArrayDeploymentSymbol.cs b/src/PSRule.Rules.Azure/Arm/Symbols/ArrayDeploymentSymbol.cs index abf9cafa342..f3436a0777f 100644 --- a/src/PSRule.Rules.Azure/Arm/Symbols/ArrayDeploymentSymbol.cs +++ b/src/PSRule.Rules.Azure/Arm/Symbols/ArrayDeploymentSymbol.cs @@ -15,13 +15,16 @@ namespace PSRule.Rules.Azure.Arm.Symbols; internal sealed class ArrayDeploymentSymbol(string name) : DeploymentSymbol(name), IDeploymentSymbol { private List? _Ids; + private List? _Resources; public DeploymentSymbolKind Kind => DeploymentSymbolKind.Array; public void Configure(IResourceValue resource) { + _Resources ??= []; + _Resources.Add(resource); _Ids ??= []; - _Ids.Add(resource.Id); + _Ids.Add(TryResourceId(resource)); } public string? GetId(int index) @@ -29,10 +32,33 @@ public void Configure(IResourceValue resource) return _Ids?[index]; } + public bool TryGetResource(int index, out IResourceValue? resource) + { + resource = _Resources != null && index >= 0 && index < _Resources.Count ? _Resources[index] : null; + return resource != null; + } + public string[] GetIds() { return _Ids?.ToArray() ?? []; } + + public IResourceValue[] GetResources() + { + return _Resources?.ToArray() ?? []; + } + + private static string TryResourceId(IResourceValue resource) + { + try + { + return resource.Id; + } + catch + { + return resource.SymbolicName; + } + } } #nullable restore diff --git a/src/PSRule.Rules.Azure/Arm/Symbols/IDeploymentSymbol.cs b/src/PSRule.Rules.Azure/Arm/Symbols/IDeploymentSymbol.cs index 1987306577e..00d7af8e9dc 100644 --- a/src/PSRule.Rules.Azure/Arm/Symbols/IDeploymentSymbol.cs +++ b/src/PSRule.Rules.Azure/Arm/Symbols/IDeploymentSymbol.cs @@ -16,6 +16,8 @@ internal interface IDeploymentSymbol void Configure(IResourceValue r); string? GetId(int index); + + bool TryGetResource(int index, out IResourceValue? resource); } #nullable restore diff --git a/src/PSRule.Rules.Azure/Arm/Symbols/ObjectDeploymentSymbol.cs b/src/PSRule.Rules.Azure/Arm/Symbols/ObjectDeploymentSymbol.cs index eb1ffbc7285..0d9908b45b7 100644 --- a/src/PSRule.Rules.Azure/Arm/Symbols/ObjectDeploymentSymbol.cs +++ b/src/PSRule.Rules.Azure/Arm/Symbols/ObjectDeploymentSymbol.cs @@ -14,6 +14,7 @@ namespace PSRule.Rules.Azure.Arm.Symbols; internal sealed class ObjectDeploymentSymbol : DeploymentSymbol, IDeploymentSymbol { private Func? _GetId; + private IResourceValue? _Resource; public ObjectDeploymentSymbol(string name, IResourceValue? resource) : base(name) @@ -26,6 +27,7 @@ public ObjectDeploymentSymbol(string name, IResourceValue? resource) public void Configure(IResourceValue resource) { + _Resource = resource; _GetId = () => resource.Id; } @@ -33,6 +35,12 @@ public void Configure(IResourceValue resource) { return _GetId == null ? null : _GetId(); } + + public bool TryGetResource(int index, out IResourceValue? resource) + { + resource = _Resource; + return resource != null; + } } #nullable restore diff --git a/tests/PSRule.Rules.Azure.Tests/Arm/Expressions/FunctionTests.cs b/tests/PSRule.Rules.Azure.Tests/Arm/Expressions/FunctionTests.cs index 44bb0da5750..69c6b761970 100644 --- a/tests/PSRule.Rules.Azure.Tests/Arm/Expressions/FunctionTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/Arm/Expressions/FunctionTests.cs @@ -752,6 +752,7 @@ public void TryGet() }; Assert.Equal("two", (Functions.TryGet(context, [testObject2, 1]) as JValue).Value()); + Assert.Null(Functions.TryGet(context, [testObject2, "addressPrefixes"])); Assert.Throws(() => Functions.TryGet(context, null)); Assert.Throws(() => Functions.TryGet(context, [])); @@ -921,6 +922,115 @@ public void Reference() Assert.Equal("a", actual["name"].Value()); } + [Fact] + [Trait(TRAIT, TRAIT_RESOURCE)] + public void ReferenceWithNetworkAddressPlaceholders() + { + var context = GetContext(); + var vnetId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Network/virtualNetworks/vnet-001"; + var subnetId = string.Concat(vnetId, "/subnets/subnet-001"); + + var vnet = Functions.Reference(context, [vnetId]) as Mock.MockObject; + Assert.NotNull(vnet); + var vnetPrefixes = vnet["addressSpace"]["addressPrefixes"] as Mock.MockArray; + Assert.NotNull(vnetPrefixes); + Assert.Equal("192.0.2.0/24", vnetPrefixes[0].Value()); + Assert.Equal("192.0.2.4", Functions.CidrHost(context, [vnetPrefixes[0], 3]) as string); + Assert.Throws(() => Functions.CidrHost(context, [vnetPrefixes, 3])); + + var subnet = Functions.Reference(context, [subnetId]) as Mock.MockObject; + Assert.NotNull(subnet); + Assert.Equal("192.0.2.0/28", subnet["addressPrefix"].Value()); + var subnetPrefixes = subnet["addressPrefixes"] as Mock.MockArray; + Assert.NotNull(subnetPrefixes); + Assert.Equal("192.0.2.0/28", subnetPrefixes[0].Value()); + Assert.Equal("192.0.2.4", Functions.CidrHost(context, [subnetPrefixes[0], 3]) as string); + + var subnetFull = Functions.Reference(context, [subnetId, "2025-07-01", "Full"]) as Mock.MockObject; + Assert.NotNull(subnetFull); + Assert.Throws(() => Functions.CidrHost(context, [subnetFull["id"], 3])); + } + + [Fact] + [Trait(TRAIT, TRAIT_RESOURCE)] + public void ReferenceWithExistingNetworkAddressSymbols() + { + var context = GetContext(); + var vnet = new ExistingResourceValue(context, "Microsoft.Network/virtualNetworks", "vnet", JObject.Parse(@" +{ + ""name"": ""vnet-001"", + ""properties"": { + ""ipamPoolPrefixAllocations"": [] + } +}"), null); + var subnet = new ExistingResourceValue(context, "Microsoft.Network/virtualNetworks/subnets", "vnet::subnet", JObject.Parse(@" +{ + ""name"": ""subnet-001"", + ""properties"": { + ""ipamPoolPrefixAllocations"": [] + } +}"), null); + var pool = new ExistingResourceValue(context, "Microsoft.Network/networkManagers/ipamPools", "networkManager::platformPool", JObject.Parse(@" +{ + ""name"": ""platformPool"" +}"), null); + context.AddSymbol(DeploymentSymbol.NewObject("vnet", vnet)); + context.AddSymbol(DeploymentSymbol.NewObject("vnet::subnet", subnet)); + context.AddSymbol(DeploymentSymbol.NewObject("networkManager::platformPool", pool)); + + var vnetProperties = Functions.Reference(context, ["vnet"]) as Mock.MockObject; + Assert.NotNull(vnetProperties); + Assert.Equal("192.0.2.0/24", vnetProperties["addressSpace"]["addressPrefixes"][0].Value()); + + var subnetProperties = Functions.Reference(context, ["vnet::subnet"]) as Mock.MockObject; + Assert.NotNull(subnetProperties); + Assert.Equal("192.0.2.0/28", subnetProperties["addressPrefixes"][0].Value()); + Assert.Equal("192.0.2.4", Functions.CidrHost(context, [subnetProperties["addressPrefixes"][0], 3]) as string); + + var poolProperties = Functions.Reference(context, ["networkManager::platformPool"]) as Mock.MockObject; + Assert.NotNull(poolProperties); + Assert.Equal("192.0.2.0/24", poolProperties["addressPrefixes"][0].Value()); + Assert.Equal("192.0.2.0/23", Functions.CidrSubnet(context, [poolProperties["addressPrefixes"][0], 23, 0]) as string); + + var subnetResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Network/virtualNetworks/vnet-001/subnets/ApplicationGatewaySubnet"; + var deployedSubnet = new ResourceValue(subnetResourceId, "ApplicationGatewaySubnet", "Microsoft.Network/virtualNetworks/subnets", "vnet::deployedSubnet", JObject.Parse(@" +{ + ""properties"": { + ""ipamPoolPrefixAllocations"": [] + } +}"), null); + context.AddResource(deployedSubnet); + context.AddSymbol(DeploymentSymbol.NewObject("vnet::deployedSubnet", deployedSubnet)); + + var deployedSubnetProperties = Functions.Reference(context, ["vnet::deployedSubnet"]) as Mock.MockObject; + Assert.NotNull(deployedSubnetProperties); + Assert.Equal("192.0.2.0/28", deployedSubnetProperties["addressPrefixes"][0].Value()); + Assert.Equal("192.0.2.4", Functions.CidrHost(context, [deployedSubnetProperties["addressPrefixes"][0], 3]) as string); + } + + [Fact] + [Trait(TRAIT, TRAIT_RESOURCE)] + public void ReferenceUsesConcreteNetworkAddressProperties() + { + var context = GetContext(); + var resourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Network/virtualNetworks/vnet-001"; + var resource = new ResourceValue(resourceId, "vnet-001", "Microsoft.Network/virtualNetworks", "vnet", JObject.Parse(@" +{ + ""properties"": { + ""addressSpace"": { + ""addressPrefixes"": [ + ""203.0.113.0/24"" + ] + } + } +}"), null); + context.AddResource(resource); + + var actual = Functions.Reference(context, [resourceId]) as Mock.MockObject; + Assert.NotNull(actual); + Assert.Equal("203.0.113.0/24", actual["addressSpace"]["addressPrefixes"][0].Value()); + } + [Fact] [Trait(TRAIT, TRAIT_RESOURCE)] public void References() diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index d540d207a37..7156ad0c8ee 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -320,6 +320,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index 04678391fc6..26d2b940cd7 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -1417,4 +1417,31 @@ public void ProcessTemplate_WhenNullableIndexFromEnd_ShouldReturnExpectedValue() var secondLastPart = secondLastPartOutput["value"].Value(); Assert.Equal("eastus", secondLastPart); } + + /// + /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3920 + /// + [Fact] + public void ProcessTemplate_WhenExistingResourceReferencedBySymbolicName_ShouldReturnMock() + { + _ = ProcessTemplate(GetSourcePath("Tests.Bicep.44.json"), null, out var templateContext); + + Assert.True(templateContext.RootDeployment.TryOutput("addressPrefixes", out JObject addressPrefixesOutput)); + Assert.Equal(JTokenType.Array, addressPrefixesOutput["value"].Type); + + Assert.True(templateContext.RootDeployment.TryOutput("combinedAddressPrefixes", out JObject combinedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, combinedAddressPrefixesOutput["value"].Type); + + Assert.True(templateContext.RootDeployment.TryOutput("mappedAddressPrefixes", out JObject mappedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, mappedAddressPrefixesOutput["value"].Type); + } + + [Fact] + public void ProcessTemplate_WhenExistingResourceReferencedByResourceId_ShouldReturnMock() + { + _ = ProcessTemplate(GetSourcePath("Tests.Bicep.45.json"), null, out var templateContext); + + Assert.True(templateContext.RootDeployment.TryOutput("combinedAddressPrefixes", out JObject combinedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, combinedAddressPrefixesOutput["value"].Type); + } } diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json new file mode 100644 index 00000000000..636be037fe5 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "resources": { + "vnet": { + "existing": true, + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-11-01", + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroup": "other-rg", + "name": "vnet1" + } + }, + "outputs": { + "addressPrefixes": { + "type": "array", + "value": "[reference('vnet').addressSpace.addressPrefixes]" + }, + "combinedAddressPrefixes": { + "type": "array", + "value": "[concat(reference('vnet').addressSpace.addressPrefixes, createArray())]" + }, + "mappedAddressPrefixes": { + "type": "array", + "value": "[map(reference('vnet').addressSpace.addressPrefixes, lambda('prefix', lambdaVariables('prefix')))]" + } + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json new file mode 100644 index 00000000000..9154ef5f629 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "combinedAddressPrefixes": { + "type": "array", + "value": "[concat(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'other-rg'), 'Microsoft.Network/virtualNetworks', 'vnet1'), '2023-11-01').addressSpace.addressPrefixes, createArray())]" + } + } +}