Skip to content

Commit 3786d84

Browse files
authored
Handle symbolic expand for existing conditional cases Azure#2917 (Azure#3083)
1 parent 27b461d commit 3786d84

25 files changed

+1305
-336
lines changed

docs/CHANGELOG-v1.md

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
2929

3030
## Unreleased
3131

32+
What's changed since pre-release v1.39.0-B0182:
33+
34+
- Bug fixes:
35+
- Fixed symbolic expand for existing with conditional cases by @BernieWhite.
36+
[#2917](https://github.com/Azure/PSRule.Rules.Azure/issues/2917)
37+
3238
## v1.39.0-B0182 (pre-release)
3339

3440
What's changed since pre-release v1.39.0-B0118:

src/PSRule.Rules.Azure/Common/JsonExtensions.cs

+127-44
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,26 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.Linq;
87
using System.Threading;
98
using Newtonsoft.Json;
109
using Newtonsoft.Json.Linq;
10+
using PSRule.Rules.Azure.Data.Template;
1111

1212
namespace PSRule.Rules.Azure
1313
{
14-
[DebuggerDisplay("Path: {Path}, Line:{LineNumber}, Position:{LinePosition}")]
15-
internal sealed class TemplateTokenAnnotation : IJsonLineInfo
16-
{
17-
private bool _HasLineInfo;
18-
19-
public TemplateTokenAnnotation()
20-
{
21-
22-
}
23-
24-
public TemplateTokenAnnotation(int lineNumber, int linePosition, string path)
25-
{
26-
SetLineInfo(lineNumber, linePosition);
27-
Path = path;
28-
}
29-
30-
public int LineNumber { get; private set; }
31-
32-
public int LinePosition { get; private set; }
33-
34-
public string Path { get; private set; }
35-
36-
public bool HasLineInfo()
37-
{
38-
return _HasLineInfo;
39-
}
40-
41-
private void SetLineInfo(int lineNumber, int linePosition)
42-
{
43-
LineNumber = lineNumber;
44-
LinePosition = linePosition;
45-
_HasLineInfo = true;
46-
}
47-
}
48-
4914
internal static class JsonExtensions
5015
{
51-
private const string PROPERTY_DEPENDSON = "dependsOn";
16+
private const string PROPERTY_DEPENDS_ON = "dependsOn";
5217
private const string PROPERTY_RESOURCES = "resources";
5318
private const string PROPERTY_NAME = "name";
5419
private const string PROPERTY_TYPE = "type";
5520
private const string PROPERTY_FIELD = "field";
21+
private const string PROPERTY_EXISTING = "existing";
22+
private const string PROPERTY_SCOPE = "scope";
23+
private const string PROPERTY_SUBSCRIPTION_ID = "subscriptionId";
24+
private const string PROPERTY_RESOURCE_GROUP = "resourceGroup";
25+
5626
private const string TARGETINFO_KEY = "_PSRule";
5727
private const string TARGETINFO_SOURCE = "source";
5828
private const string TARGETINFO_FILE = "file";
@@ -66,7 +36,9 @@ internal static class JsonExtensions
6636
private const string TARGETINFO_PATH = "path";
6737
private const string TARGETINFO_MESSAGE = "message";
6838

69-
private static readonly string[] JSON_PATH_SEPARATOR = new string[] { "." };
39+
private const string TENANT_SCOPE = "/";
40+
41+
private static readonly string[] JSON_PATH_SEPARATOR = ["."];
7042

7143
internal static IJsonLineInfo TryLineInfo(this JToken token)
7244
{
@@ -368,7 +340,7 @@ private static string JsonPathJoin(string parent, string child)
368340
internal static bool TryGetDependencies(this JObject resource, out string[] dependencies)
369341
{
370342
dependencies = null;
371-
if (!(resource.ContainsKey(PROPERTY_DEPENDSON) && resource[PROPERTY_DEPENDSON] is JArray d && d.Count > 0))
343+
if (!(resource.ContainsKey(PROPERTY_DEPENDS_ON) && resource[PROPERTY_DEPENDS_ON] is JArray d && d.Count > 0))
372344
return false;
373345

374346
dependencies = d.Values<string>().ToArray();
@@ -388,19 +360,19 @@ internal static string GetResourcePath(this JObject resource, int parentLevel =
388360

389361
internal static void SetTargetInfo(this JObject resource, string templateFile, string parameterFile, string path = null)
390362
{
391-
// Get line infomation
363+
// Get line information.
392364
var lineInfo = resource.TryLineInfo();
393365

394-
// Populate target info
366+
// Populate target info.
395367
resource.UseProperty(TARGETINFO_KEY, out JObject targetInfo);
396368

397-
// Path
369+
// Path.
398370
path ??= resource.GetResourcePath();
399371
targetInfo.Add(TARGETINFO_PATH, path);
400372

401373
var sources = new JArray();
402374

403-
// Template file
375+
// Template file.
404376
if (!string.IsNullOrEmpty(templateFile))
405377
{
406378
var source = new JObject
@@ -415,7 +387,8 @@ internal static void SetTargetInfo(this JObject resource, string templateFile, s
415387
}
416388
sources.Add(source);
417389
}
418-
// Parameter file
390+
391+
// Parameter file.
419392
if (!string.IsNullOrEmpty(parameterFile))
420393
{
421394
var source = new JObject
@@ -534,5 +507,115 @@ internal static bool IsEmpty(this JToken value)
534507
value.Type == JTokenType.Null ||
535508
(value.Type == JTokenType.String && string.IsNullOrEmpty(value.Value<string>()));
536509
}
510+
511+
internal static bool IsExisting(this JObject o)
512+
{
513+
return o != null && o.TryBoolProperty(PROPERTY_EXISTING, out var existing) && existing != null && existing.Value;
514+
}
515+
516+
internal static bool TryResourceType(this JObject o, out string type)
517+
{
518+
type = default;
519+
return o != null && o.TryGetProperty(PROPERTY_TYPE, out type);
520+
}
521+
522+
internal static bool TryResourceName(this JObject o, out string name)
523+
{
524+
name = default;
525+
return o != null && o.TryGetProperty(PROPERTY_NAME, out name);
526+
}
527+
528+
internal static bool TryResourceNameAndType(this JObject o, out string name, out string type)
529+
{
530+
name = default;
531+
type = default;
532+
return o != null && o.TryResourceName(out name) && o.TryResourceType(out type);
533+
}
534+
535+
/// <summary>
536+
/// Read the scope from a specified <c>scope</c> property.
537+
/// </summary>
538+
/// <param name="resource">The resource object.</param>
539+
/// <param name="context">A valid context to resolve properties.</param>
540+
/// <param name="scopeId">The scope if set.</param>
541+
/// <returns>Returns <c>true</c> if the scope property was set on the resource.</returns>
542+
internal static bool TryResourceScope(this JObject resource, ITemplateContext context, out string scopeId)
543+
{
544+
scopeId = default;
545+
if (resource == null)
546+
return false;
547+
548+
return TryExplicitScope(resource, context, out scopeId)
549+
|| TryExplicitSubscriptionResourceGroupScope(resource, context, out scopeId)
550+
|| TryParentScope(resource, context, out scopeId)
551+
|| TryDeploymentScope(context, out scopeId);
552+
}
553+
554+
private static bool TryExplicitScope(JObject resource, ITemplateContext context, out string scopeId)
555+
{
556+
scopeId = context.ExpandProperty<string>(resource, PROPERTY_SCOPE);
557+
if (string.IsNullOrEmpty(scopeId))
558+
return false;
559+
560+
// Check for full scope.
561+
ResourceHelper.ResourceIdComponents(scopeId, out var tenant, out var managementGroup, out var subscriptionId, out var resourceGroup, out var resourceType, out var resourceName);
562+
if (tenant != null || managementGroup != null || subscriptionId != null)
563+
return true;
564+
565+
scopeId = ResourceHelper.ResourceId(resourceType, resourceName, scopeId: context.ScopeId);
566+
return true;
567+
}
568+
569+
/// <summary>
570+
/// Get the scope from <c>subscriptionId</c> and <c>resourceGroup</c> properties set on the resource.
571+
/// </summary>
572+
private static bool TryExplicitSubscriptionResourceGroupScope(JObject resource, ITemplateContext context, out string scopeId)
573+
{
574+
var subscriptionId = context.ExpandProperty<string>(resource, PROPERTY_SUBSCRIPTION_ID);
575+
var resourceGroup = context.ExpandProperty<string>(resource, PROPERTY_RESOURCE_GROUP);
576+
577+
// Fill subscriptionId if resourceGroup is specified.
578+
if (!string.IsNullOrEmpty(resourceGroup) && string.IsNullOrEmpty(subscriptionId))
579+
subscriptionId = context.Subscription.SubscriptionId;
580+
581+
scopeId = !string.IsNullOrEmpty(subscriptionId) ? ResourceHelper.ResourceId(scopeTenant: null, scopeManagementGroup: null, scopeSubscriptionId: subscriptionId, scopeResourceGroup: resourceGroup, resourceType: null, resourceName: null) : null;
582+
return scopeId != null;
583+
}
584+
585+
/// <summary>
586+
/// Read the scope from the name and type properties if this is a sub-resource.
587+
/// For example: A sub-resource may use name segments such as <c>vnet-1/subnet-1</c>.
588+
/// </summary>
589+
/// <param name="resource">The resource object.</param>
590+
/// <param name="context">A valid context to resolve properties.</param>
591+
/// <param name="scopeId">The calculated scope.</param>
592+
/// <returns>Returns <c>true</c> if the scope could be calculated from name segments.</returns>
593+
private static bool TryParentScope(JObject resource, ITemplateContext context, out string scopeId)
594+
{
595+
scopeId = null;
596+
var name = context.ExpandProperty<string>(resource, PROPERTY_NAME);
597+
var type = context.ExpandProperty<string>(resource, PROPERTY_TYPE);
598+
599+
if (string.IsNullOrEmpty(name) ||
600+
string.IsNullOrEmpty(type) ||
601+
!ResourceHelper.TryResourceIdComponents(type, name, out var resourceTypeComponents, out var nameComponents) ||
602+
resourceTypeComponents.Length == 1)
603+
return false;
604+
605+
scopeId = ResourceHelper.GetParentResourceId(context.Subscription.SubscriptionId, context.ResourceGroup.Name, resourceTypeComponents, nameComponents);
606+
return true;
607+
}
608+
609+
/// <summary>
610+
/// Get the scope of the resource based on the scope of the deployment.
611+
/// </summary>
612+
/// <param name="context">A valid context to resolve the deployment scope.</param>
613+
/// <param name="scopeId">The scope of the deployment.</param>
614+
/// <returns>Returns <c>true</c> if a deployment scope was found.</returns>
615+
private static bool TryDeploymentScope(ITemplateContext context, out string scopeId)
616+
{
617+
scopeId = context.Deployment?.Scope;
618+
return scopeId != null;
619+
}
537620
}
538621
}

0 commit comments

Comments
 (0)