Skip to content

Commit

Permalink
Fixed support for references function #2922 (#2958)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Jun 29, 2024
1 parent a723b9b commit 3a2e398
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 18 deletions.
3 changes: 1 addition & 2 deletions bicepconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"experimentalFeaturesEnabled": {
"optionalModuleNames": true,
"userDefinedFunctions": true
"optionalModuleNames": true
}
}
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ What's changed since pre-release v1.38.0-B0034:
- Engineering:
- Quality updates to rule documentation by @BernieWhite.
[#2570](https://github.com/Azure/PSRule.Rules.Azure/issues/2570)
- Bug fixes:
- Fixed support for `references` function by @BernieWhite.
[#2922](https://github.com/Azure/PSRule.Rules.Azure/issues/2922)

## v1.38.0-B0034 (pre-release)

Expand Down
6 changes: 6 additions & 0 deletions src/PSRule.Rules.Azure/Data/Policy/PolicyAssignmentVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ public bool TryGetResource(string resourceId, out IResourceValue resource)
return false;
}

public bool TryGetResourceCollection(string symbolicName, out IResourceValue[] resources)
{
resources = null;
return false;
}

internal bool TryParameterAssignment(string parameterName, out JToken value)
{
return _ParameterAssignments.TryGetValue(parameterName, out value);
Expand Down
16 changes: 11 additions & 5 deletions src/PSRule.Rules.Azure/Data/Template/ArrayDeploymentSymbol.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace PSRule.Rules.Azure.Data.Template
{
internal sealed class ArrayDeploymentSymbol : DeploymentSymbol, IDeploymentSymbol
{
private List<ResourceValue> _Resources;
private List<string> _Ids;

public ArrayDeploymentSymbol(string name)
: base(name) { }
Expand All @@ -16,13 +17,18 @@ public ArrayDeploymentSymbol(string name)

public void Configure(ResourceValue resource)
{
_Resources ??= new List<ResourceValue>();
_Resources.Add(resource);
_Ids ??= new List<string>();
_Ids.Add(resource.Id);
}

public string GetId(int index)
{
return _Resources[index].Id;
return _Ids[index];
}

public string[] GetIds()
{
return _Ids?.ToArray() ?? Array.Empty<string>();
}
}
}
37 changes: 37 additions & 0 deletions src/PSRule.Rules.Azure/Data/Template/Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ internal static class Functions
new FunctionDescriptor("pickZones", PickZones),
new FunctionDescriptor("providers", Providers),
new FunctionDescriptor("reference", Reference),
new FunctionDescriptor("references", References),
new FunctionDescriptor("resourceId", ResourceId),
new FunctionDescriptor("subscriptionResourceId", SubscriptionResourceId),
new FunctionDescriptor("tenantResourceId", TenantResourceId),
Expand Down Expand Up @@ -1013,6 +1014,37 @@ internal static object Reference(ITemplateContext context, object[] args)
: full ? new Mock.MockResource(resourceId) : new Mock.MockResource(resourceId)["properties"];
}

/// <summary>
/// references(symbolic name of a resource collection, ['Full', 'Properties'])
/// </summary>
/// <remarks>
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-resource#references"/>.
/// </remarks>
internal static object References(ITemplateContext context, object[] args)
{
var argCount = CountArgs(args);
if (argCount is < 1 or > 2)
throw ArgumentsOutOfRange(nameof(References), args);

string fullValue = null;
var full = argCount == 2 && ExpressionHelpers.TryString(args[1], out fullValue) && string.Equals(fullValue, PROPERTY_FULL, StringComparison.OrdinalIgnoreCase);
if (argCount == 2 && !full && !string.Equals(fullValue, PROPERTY_PROPERTIES, StringComparison.OrdinalIgnoreCase))
throw ArgumentFormatInvalid(nameof(References));

// Get symbolic name
if (!ExpressionHelpers.TryString(args[0], out var symbolicName))
throw ArgumentFormatInvalid(nameof(References));

if (!context.TryGetResourceCollection(symbolicName, out var resources))
throw ArgumentInvalidResourceCollection(nameof(References), symbolicName);

var result = new object[resources.Length];
for (var i = 0; i < resources.Length; i++)
result[i] = GetReferenceResult(resources[i], full);

return result;
}

private static object GetReferenceResult(IResourceValue resource, bool full)
{
if (resource is DeploymentValue deployment)
Expand Down Expand Up @@ -2481,6 +2513,11 @@ private static ExpressionArgumentException ArgumentNullNotExpected(string expres
);
}

private static Exception ArgumentInvalidResourceCollection(string v, string symbolicName)
{
throw new NotImplementedException();
}

#endregion Exceptions
}
}
21 changes: 20 additions & 1 deletion src/PSRule.Rules.Azure/Data/Template/ITemplateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,27 @@ internal interface ITemplateContext : IValidationContext

bool TryDefinition(string type, out ITypeDefinition definition);

bool TryGetResource(string resourceId, out IResourceValue resource);
/// <summary>
/// Try to get a resource from the current context.
/// </summary>
/// <param name="nameOrResourceId">The symbolic name or resource identifier.</param>
/// <param name="resource">A resource.</param>
/// <returns>Returns <c>true</c> when the resource exists.</returns>
bool TryGetResource(string nameOrResourceId, out IResourceValue resource);

/// <summary>
/// Try to get a resource collection from the current context.
/// </summary>
/// <param name="symbolicName">The symbolic name for the collection.</param>
/// <param name="resources">A collection of resources.</param>
/// <returns>Returns <c>true</c> when the symbolic name exists.</returns>
bool TryGetResourceCollection(string symbolicName, out IResourceValue[] resources);

/// <summary>
/// Write a debug message for the current execution context.
/// </summary>
/// <param name="message">The format message.</param>
/// <param name="args">Additional arguments for the format message.</param>
void WriteDebug(string message, params object[] args);

/// <summary>
Expand Down
12 changes: 10 additions & 2 deletions src/PSRule.Rules.Azure/Data/Template/NestedTemplateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,19 @@ public virtual bool TryVariable(string variableName, out object value)
return _Inner.TryVariable(variableName, out value);
}

public bool TryGetResource(string resourceId, out IResourceValue resource)
/// <inheritdoc/>
public bool TryGetResource(string nameOrResourceId, out IResourceValue resource)
{
return _Inner.TryGetResource(nameOrResourceId, out resource);
}

/// <inheritdoc/>
public bool TryGetResourceCollection(string symbolicName, out IResourceValue[] resources)
{
return _Inner.TryGetResource(resourceId, out resource);
return _Inner.TryGetResourceCollection(symbolicName, out resources);
}

/// <inheritdoc/>
public void WriteDebug(string message, params object[] args)
{
_Inner.WriteDebug(message, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Rules.Azure.Data.Template
{
internal sealed class ObjectDeploymentSymbol : DeploymentSymbol, IDeploymentSymbol
{
private ResourceValue _Resource;
private string _ResourceId;

public ObjectDeploymentSymbol(string name, ResourceValue resource)
: base(name)
Expand All @@ -18,12 +18,12 @@ public ObjectDeploymentSymbol(string name, ResourceValue resource)

public void Configure(ResourceValue resource)
{
_Resource = resource;
_ResourceId = resource.Id;
}

public string GetId(int index)
{
return _Resource.Id;
return _ResourceId;
}
}
}
24 changes: 20 additions & 4 deletions src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,34 @@ public void RemoveResource(IResourceValue resource)
_ResourceIds.Remove(resource.Id);
}

public bool TryGetResource(string resourceId, out IResourceValue resource)
/// <inheritdoc/>
public bool TryGetResource(string nameOrResourceId, out IResourceValue resource)
{
if (_Symbols.TryGetValue(resourceId, out var symbol))
resourceId = symbol.GetId(0);
if (_Symbols.TryGetValue(nameOrResourceId, out var symbol))
nameOrResourceId = symbol.GetId(0);

if (_ResourceIds.TryGetValue(resourceId, out resource))
if (_ResourceIds.TryGetValue(nameOrResourceId, out resource))
return true;

resource = null;
return false;
}

/// <inheritdoc/>
public bool TryGetResourceCollection(string symbolicName, out IResourceValue[] resources)
{
resources = null;
if (!_Symbols.TryGetValue(symbolicName, out var symbol) || symbol is not ArrayDeploymentSymbol array)
return false;

var ids = array.GetIds();
resources = new IResourceValue[ids.Length];
for (var i = 0; i < ids.Length; i++)
resources[i] = _ResourceIds[ids[i]];

return true;
}

public void AddOutput(string name, JObject output)
{
if (string.IsNullOrEmpty(name) || output == null || _CurrentDeployment == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2922

module child_loop './Tests.Bicep.1.child.bicep' = [
for (item, index) in range(0, 2): {
name: 'deploy-${index}'
params: {
index: index
}
}
]

output items array = map(child_loop, item => item.outputs.childValue)
output itemsAsString string[] = map(child_loop, item => item.outputs.childValue)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

param index int

output childValue string = 'child-${index}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.28.1.47646",
"templateHash": "1381069533852459453"
}
},
"resources": {
"child_loop": {
"copy": {
"name": "child_loop",
"count": "[length(range(0, 2))]"
},
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-09-01",
"name": "[format('deploy-{0}', copyIndex())]",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"index": {
"value": "[copyIndex()]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.28.1.47646",
"templateHash": "15231390219392886169"
}
},
"parameters": {
"index": {
"type": "int"
}
},
"resources": {},
"outputs": {
"childValue": {
"type": "string",
"value": "[format('child-{0}', parameters('index'))]"
}
}
}
}
}
},
"outputs": {
"items": {
"type": "array",
"value": "[map(references('child_loop'), lambda('item', lambdaVariables('item').outputs.childValue.value))]"
},
"itemsAsString": {
"type": "array",
"items": {
"type": "string"
},
"value": "[map(references('child_loop'), lambda('item', lambdaVariables('item').outputs.childValue.value))]"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"experimentalFeaturesEnabled": {
"optionalModuleNames": true,
"symbolicNameCodegen": true
}
}
24 changes: 24 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Configuration;
Expand Down Expand Up @@ -810,6 +811,29 @@ public void Reference()
Assert.Equal("a", actual["name"].Value<string>());
}

[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void References()
{
var context = GetContext();
var resourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Unit.Test/type/a";
var resource = new ResourceValue(resourceId, "a-0", "Unit.Test/type", "child_loop[0]", new JObject(), null);
context.AddResource(resource);

var symbol = DeploymentSymbol.NewArray("child_loop");
symbol.Configure(resource);
context.AddSymbol(symbol);

var actual = (Functions.References(context, new object[] { "child_loop" }) as object[]).OfType<Mock.MockObject>().ToArray();
Assert.NotNull(actual);

actual = (Functions.References(context, new object[] { "child_loop", "Full" }) as object[]).OfType<Mock.MockObject>().ToArray();
Assert.NotNull(actual);

Assert.Throws<ExpressionArgumentException>(() => Functions.References(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.References(context, new object[] { "Unit.Test/type", "test" }));
}

[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void ResourceId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@
<None Update="Tests.Bicep.40.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Bicep\SymbolicNameTestCases\Tests.Bicep.1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tests.Bicep.5.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading

0 comments on commit 3a2e398

Please sign in to comment.