зеркало из
1
0
Форкнуть 0

Added support for providers and dateTimeAdd functions #177 #516 (#519)

This commit is contained in:
Bernie White 2020-09-28 23:53:50 +10:00 коммит произвёл GitHub
Родитель e6fa61641b
Коммит 871b94b671
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 57698 добавлений и 102 удалений

11
.vscode/tasks.json поставляемый
Просмотреть файл

@ -4,7 +4,7 @@
"version": "2.0.0",
"tasks": [
{
"label": "test",
"label": "Test",
"detail": "Build and run unit tests.",
"type": "shell",
"command": "Invoke-Build Test -AssertStyle Client",
@ -31,7 +31,7 @@
}
},
{
"label": "build",
"label": "Build",
"detail": "Build module.",
"type": "shell",
"command": "Invoke-Build Build",
@ -86,6 +86,13 @@
"type": "shell",
"command": "Invoke-Build BuildBaselineDocs",
"problemMatcher": []
},
{
"label": "Export data",
"detail": "Export a list of resource providers from an Azure subscription.",
"type": "shell",
"command": "Invoke-Build ExportData",
"problemMatcher": []
}
]
}

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

@ -2,6 +2,10 @@
## Unreleased
- General improvements:
- Added support for `providers` template function. [#177](https://github.com/Microsoft/PSRule.Rules.Azure/issues/177)
- Added support for `dateTimeAdd` template function. [#516](https://github.com/Microsoft/PSRule.Rules.Azure/issues/516)
## v0.16.0
What's changed since v0.15.0:

33
data/environments.json Normal file
Просмотреть файл

@ -0,0 +1,33 @@
{
"AzureCloud": {
"name": "AzureCloud",
"gallery": "https://gallery.azure.com/",
"graph": "https://graph.windows.net/",
"portal": "https://portal.azure.com",
"graphAudience": "https://graph.windows.net/",
"activeDirectoryDataLake": "https://datalake.azure.net/",
"batch": "https://batch.core.windows.net/",
"media": "https://rest.media.azure.net",
"sqlManagement": "https://management.core.windows.net:8443/",
"vmImageAliasDoc": "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json",
"resourceManager": "https://management.azure.com/",
"authentication": {
"loginEndpoint": "https://login.microsoftonline.com/",
"audiences": [
"https://management.core.windows.net/",
"https://management.azure.com/"
],
"tenant": "common",
"identityProvider": "AAD"
},
"suffixes": {
"acrLoginServer": ".azurecr.io",
"azureDatalakeAnalyticsCatalogAndJob": "azuredatalakeanalytics.net",
"azureDatalakeStoreFileSystem": "azuredatalakestore.net",
"azureFrontDoorEndpointSuffix": "azurefd.net",
"keyvaultDns": ".vault.azure.net",
"sqlServerHostname": ".database.windows.net",
"storage": "core.windows.net"
}
}
}

54348
data/providers.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -35,7 +35,7 @@ Currently the following limitations also apply:
- Nested templates are expanded, external templates are not.
- Deployment resources that link to an external template are returned as a resource.
- The `providers` template function is not supported.
- The `environment`, and `pickZones` template functions are not supported.
- References to Key Vault secrets are not expanded. A placeholder value is used instead.
- Multi-line strings and user-defined functions are not supported.

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

@ -212,7 +212,25 @@ task TestDotNet {
}
}
task CopyModule {
task MinifyData {
$dataPath = Join-Path -Path $PWD -ChildPath 'data';
$dataOutPath = Join-Path -Path $PWD -ChildPath 'out/data';
if (!(Test-Path -Path $dataOutPath)) {
$Null = New-Item -Path $dataOutPath -ItemType Directory -Force;
}
# Providers
$filePath = Join-Path -Path $dataPath -ChildPath 'providers.json';
$fileOutPath = Join-Path -Path $dataOutPath -ChildPath 'providers.json';
Get-Content -Path $filePath -Raw | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress | Set-Content -Path $fileOutPath;
# Environments
$filePath = Join-Path -Path $dataPath -ChildPath 'environments.json';
$fileOutPath = Join-Path -Path $dataOutPath -ChildPath 'environments.json';
Get-Content -Path $filePath -Raw | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress | Set-Content -Path $fileOutPath;
}
task CopyModule MinifyData, {
CopyModuleFiles -Path src/PSRule.Rules.Azure -DestinationPath out/modules/PSRule.Rules.Azure;
# Copy LICENSE
@ -220,6 +238,9 @@ task CopyModule {
# Copy third party notices
Copy-Item -Path ThirdPartyNotices.txt -Destination out/modules/PSRule.Rules.Azure;
# Copy data
Copy-Item -Path 'out/data/*.json' -Destination out/modules/PSRule.Rules.Azure -Recurse;
}
# Synopsis: Build modules only
@ -249,6 +270,26 @@ task TestModule ModuleDependencies, Pester, PSScriptAnalyzer, {
}
}
task IntegrationTest ModuleDependencies, Pester, PSScriptAnalyzer, {
# Run Pester tests
$pesterParams = @{ Path = (Join-Path -Path $PWD -ChildPath tests/Integration); OutputFile = 'reports/pester-unit.xml'; OutputFormat = 'NUnitXml'; PesterOption = @{ IncludeVSCodeMarker = $True }; PassThru = $True; };
if (!(Test-Path -Path reports)) {
$Null = New-Item -Path reports -ItemType Directory -Force;
}
$results = Invoke-Pester @pesterParams;
# Throw an error if pester tests failed
if ($Null -eq $results) {
throw 'Failed to get Pester test results.';
}
elseif ($results.FailedCount -gt 0) {
throw "$($results.FailedCount) tests failed.";
}
}
# Synopsis: Run validation
task Rules PSRule, {
$assertParams = @{
@ -327,6 +368,38 @@ task ScaffoldHelp Build, BuildRuleDocs, {
Update-MarkdownHelp -Path '.\docs\commands\PSRule.Rules.Azure\en-US';
}
task ExportProviders {
$providers = Get-AzResourceProvider -ListAvailable;
$index = @{}
foreach ($provider in $providers) {
$namespace = $provider.ProviderNamespace;
$provider.PSObject.Properties.Remove('RegistrationState');
$provider.PSObject.Properties.Remove('ProviderNamespace');
$resourceTypes = @{};
$provider.ResourceTypes | ForEach-Object {
$info = @{
resourceType = $_.ResourceTypeName
apiVersions = $_.ApiVersions
locations = $_.Locations
}
$resourceTypes.Add($info.resourceType, $info);
};
$entry = @{
types = $resourceTypes
}
$index.Add($namespace, $entry);
}
$dataPath = Join-Path -Path $PWD -ChildPath 'data';
if (!(Test-Path -Path $dataPath)) {
$Null = New-Item -Path $dataPath -ItemType Directory -Force;
}
$index | ConvertTo-Json -Depth 20 | Set-Content ./data/providers.json;
}
task ExportData ExportProviders
# Synopsis: Add shipit build tag
task TagBuild {
if ($Null -ne $Env:BUILD_DEFINITIONNAME) {

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

@ -2,6 +2,7 @@
// Licensed under the MIT License.
using Newtonsoft.Json;
using PSRule.Rules.Azure.Data.Template;
using PSRule.Rules.Azure.Pipeline;
using PSRule.Rules.Azure.Resources;
using System;
@ -221,4 +222,116 @@ namespace PSRule.Rules.Azure
return result.ToArray();
}
}
internal sealed class ResourceProviderConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ResourceProvider) || objectType == typeof(Dictionary<string, ResourceProvider>);
}
public override bool CanRead => true;
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (objectType == typeof(ResourceProvider))
{
var resultObject = existingValue as ResourceProvider ?? new ResourceProvider();
ReadObject(resultObject, reader, serializer);
return resultObject;
}
var resultDictionary = existingValue as Dictionary<string, ResourceProvider> ?? new Dictionary<string, ResourceProvider>(StringComparer.OrdinalIgnoreCase);
ReadDictionary(resultDictionary, reader, serializer);
return resultDictionary;
}
private static void ReadObject(ResourceProvider value, JsonReader reader, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
string name = null;
// Read each token
while (reader.TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.StartObject:
if (name == "types")
{
ReadType(value, reader, serializer);
}
break;
}
reader.Read();
}
}
private static void ReadType(ResourceProvider value, JsonReader reader, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
string name = null;
// Read each token
while (reader.TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.StartObject:
var resourceType = serializer.Deserialize<ResourceProviderType>(reader);
value.Types.Add(name, resourceType);
break;
}
reader.Read();
}
}
private static void ReadDictionary(Dictionary<string, ResourceProvider> value, JsonReader reader, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
string name = null;
// Read each token
while (reader.TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.StartObject:
var provider = new ResourceProvider();
ReadObject(provider, reader, serializer);
value.Add(name, provider);
break;
}
reader.Read();
}
}
}
}

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

@ -14,6 +14,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Web;
using System.Xml;
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
namespace PSRule.Rules.Azure.Data.Template
@ -27,14 +28,12 @@ namespace PSRule.Rules.Azure.Data.Template
{
// Array and object
new FunctionDescriptor("array", Array),
new FunctionDescriptor("coalesce", Coalesce),
new FunctionDescriptor("concat", Concat),
new FunctionDescriptor("contains", Contains),
new FunctionDescriptor("createArray", CreateArray),
new FunctionDescriptor("empty", Empty),
new FunctionDescriptor("first", First),
new FunctionDescriptor("intersection", Intersection),
new FunctionDescriptor("json", Json),
new FunctionDescriptor("last", Last),
new FunctionDescriptor("length", Length),
new FunctionDescriptor("min", Min),
@ -45,14 +44,20 @@ namespace PSRule.Rules.Azure.Data.Template
new FunctionDescriptor("union", Union),
// Comparison
new FunctionDescriptor("coalesce", Coalesce),
new FunctionDescriptor("equals", Equals),
new FunctionDescriptor("greater", Greater),
new FunctionDescriptor("greaterOrEquals", GreaterOrEquals),
new FunctionDescriptor("less", Less),
new FunctionDescriptor("lessOrEquals", LessOrEquals),
// Date
new FunctionDescriptor("dateTimeAdd", DateTimeAdd),
new FunctionDescriptor("utcNow", UtcNow),
// Deployment
new FunctionDescriptor("deployment", Deployment),
// environment
new FunctionDescriptor("parameters", Parameters),
new FunctionDescriptor("variables", Variables),
@ -75,10 +80,14 @@ namespace PSRule.Rules.Azure.Data.Template
new FunctionDescriptor("mul", Mul),
new FunctionDescriptor("sub", Sub),
// Object
new FunctionDescriptor("json", Json),
// Resource
new FunctionDescriptor("extensionResourceId", ExtensionResourceId),
new FunctionDescriptor("list", List), // Includes listAccountSas, listKeys, listSecrets, list*
// providers
// pickZones
new FunctionDescriptor("providers", Providers),
new FunctionDescriptor("reference", Reference),
new FunctionDescriptor("resourceGroup", ResourceGroup),
new FunctionDescriptor("resourceId", ResourceId),
@ -119,7 +128,6 @@ namespace PSRule.Rules.Azure.Data.Template
new FunctionDescriptor("uri", Uri),
new FunctionDescriptor("uriComponent", UriComponent),
new FunctionDescriptor("uriComponentToString", UriComponentToString),
new FunctionDescriptor("utcNow", UtcNow),
};
private static readonly CultureInfo AzureCulture = new CultureInfo("en-US");
@ -395,15 +403,15 @@ namespace PSRule.Rules.Azure.Data.Template
if (CountArgs(args) != 2)
throw ArgumentsOutOfRange(nameof(Range), args);
if (!ExpressionHelpers.TryInt(args[0], out int startingInteger))
throw new ArgumentException();
if (!ExpressionHelpers.TryInt(args[0], out int startIndex))
throw ArgumentInvalidInteger(nameof(Range), nameof(startIndex));
if (!ExpressionHelpers.TryInt(args[1], out int numberofElements))
throw new ArgumentException();
if (!ExpressionHelpers.TryInt(args[1], out int count))
throw ArgumentInvalidInteger(nameof(Range), nameof(count));
var result = new int[numberofElements];
for (var i = 0; i < numberofElements; i++)
result[i] = startingInteger++;
var result = new int[count];
for (var i = 0; i < count; i++)
result[i] = startIndex++;
return new JArray(result);
}
@ -414,7 +422,7 @@ namespace PSRule.Rules.Azure.Data.Template
throw ArgumentsOutOfRange(nameof(Skip), args);
if (!ExpressionHelpers.TryInt(args[1], out int numberToSkip))
throw new ArgumentException();
throw ArgumentInvalidInteger(nameof(Skip), nameof(numberToSkip));
int skip = numberToSkip <= 0 ? 0 : numberToSkip;
if (ExpressionHelpers.TryString(args[0], out string soriginalValue))
@ -435,7 +443,6 @@ namespace PSRule.Rules.Azure.Data.Template
return result;
}
throw new ArgumentException();
}
@ -633,6 +640,29 @@ namespace PSRule.Rules.Azure.Data.Template
return new MockList(resourceId);
}
internal static object Providers(TemplateContext context, object[] args)
{
var argCount = CountArgs(args);
if (argCount < 1 || argCount > 2)
throw ArgumentsOutOfRange(nameof(Providers), args);
if (!ExpressionHelpers.TryString(args[0], out string providerNamespace))
throw ArgumentFormatInvalid(nameof(Providers));
string resourceType = null;
if (argCount > 1 && !ExpressionHelpers.TryString(args[1], out resourceType))
throw ArgumentFormatInvalid(nameof(Providers));
var resourceTypes = context.GetResourceType(providerNamespace, resourceType);
if (resourceType == null)
return resourceTypes;
if (resourceTypes == null || resourceTypes.Length == 0)
throw ArgumentInvalidResourceType(nameof(Providers), providerNamespace, resourceType);
return resourceTypes[0];
}
internal static object Reference(TemplateContext context, object[] args)
{
var argCount = CountArgs(args);
@ -1004,6 +1034,45 @@ namespace PSRule.Rules.Azure.Data.Template
#endregion Comparison
#region Date
internal static object DateTimeAdd(TemplateContext context, object[] args)
{
var argCount = CountArgs(args);
if (argCount < 2 || argCount > 3)
throw ArgumentsOutOfRange(nameof(DateTimeAdd), args);
if (!ExpressionHelpers.TryString(args[0], out string start))
throw ArgumentInvalidString(nameof(DateTimeAdd), "base");
if (!ExpressionHelpers.TryString(args[1], out string duration))
throw ArgumentInvalidString(nameof(DateTimeAdd), nameof(duration));
string format = null;
if (argCount == 3 && !ExpressionHelpers.TryString(args[2], out format))
throw ArgumentInvalidString(nameof(DateTimeAdd), nameof(format));
var startTime = DateTime.Parse(start, AzureCulture);
var timeToAdd = XmlConvert.ToTimeSpan(duration);
var result = startTime.Add(timeToAdd);
return format == null ? result.ToString(AzureCulture) : result.ToString(format, AzureCulture);
}
internal static object UtcNow(TemplateContext context, object[] args)
{
var argCount = CountArgs(args);
if (CountArgs(args) > 1)
throw ArgumentsOutOfRange(nameof(UtcNow), args);
var format = "yyyyMMddTHHmmssZ";
if (argCount == 1 && !ExpressionHelpers.TryString(args[0], out format))
throw ArgumentInvalidString(nameof(UtcNow), nameof(format));
return DateTime.UtcNow.ToString(format, AzureCulture);
}
#endregion Date
#region Logical
/// <summary>
@ -1089,10 +1158,10 @@ namespace PSRule.Rules.Azure.Data.Template
if (CountArgs(args) != 1)
throw ArgumentsOutOfRange(nameof(Base64), args);
if (!ExpressionHelpers.TryString(args[0], out string value))
throw new ArgumentException();
if (!ExpressionHelpers.TryString(args[0], out string inputString))
throw ArgumentInvalidString(nameof(Base64), nameof(inputString));
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
return Convert.ToBase64String(Encoding.UTF8.GetBytes(inputString));
}
internal static object Base64ToJson(TemplateContext context, object[] args)
@ -1100,10 +1169,10 @@ namespace PSRule.Rules.Azure.Data.Template
if (CountArgs(args) != 1)
throw ArgumentsOutOfRange(nameof(Base64ToJson), args);
if (!ExpressionHelpers.TryString(args[0], out string value))
throw new ArgumentException();
if (!ExpressionHelpers.TryString(args[0], out string base64Value))
throw ArgumentInvalidString(nameof(Base64ToJson), nameof(base64Value));
return JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(value)));
return JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(base64Value)));
}
internal static object Base64ToString(TemplateContext context, object[] args)
@ -1111,10 +1180,10 @@ namespace PSRule.Rules.Azure.Data.Template
if (CountArgs(args) != 1)
throw ArgumentsOutOfRange(nameof(Base64ToString), args);
if (!ExpressionHelpers.TryString(args[0], out string value))
throw new ArgumentException();
if (!ExpressionHelpers.TryString(args[0], out string base64Value))
throw ArgumentInvalidString(nameof(Base64ToString), nameof(base64Value));
return Encoding.UTF8.GetString(Convert.FromBase64String(value));
return Encoding.UTF8.GetString(Convert.FromBase64String(base64Value));
}
internal static object DataUri(TemplateContext context, object[] args)
@ -1326,10 +1395,10 @@ namespace PSRule.Rules.Azure.Data.Template
throw ArgumentsOutOfRange(nameof(Uri), args);
if (!ExpressionHelpers.TryString(args[0], out string baseUri))
throw new ArgumentException(PSRuleResources.FunctionInvalidString, "baseUri");
throw ArgumentInvalidString(nameof(Uri), "baseUri");
if (!ExpressionHelpers.TryString(args[1], out string relativeUri))
throw new ArgumentException(PSRuleResources.FunctionInvalidString, "relativeUri");
throw ArgumentInvalidString(nameof(Uri), "relativeUri");
var result = new Uri(new Uri(baseUri), relativeUri);
return result.ToString();
@ -1341,7 +1410,7 @@ namespace PSRule.Rules.Azure.Data.Template
throw ArgumentsOutOfRange(nameof(UriComponent), args);
if (!ExpressionHelpers.TryString(args[0], out string stringToEncode))
throw new ArgumentException(PSRuleResources.FunctionInvalidString, "stringToEncode");
throw ArgumentInvalidString(nameof(UriComponent), "stringToEncode");
return HttpUtility.UrlEncode(stringToEncode);
}
@ -1352,24 +1421,11 @@ namespace PSRule.Rules.Azure.Data.Template
throw ArgumentsOutOfRange(nameof(UriComponentToString), args);
if (!ExpressionHelpers.TryString(args[0], out string uriEncodedString))
throw new ArgumentException(PSRuleResources.FunctionInvalidString, "uriEncodedString");
throw ArgumentInvalidString(nameof(UriComponentToString), "uriEncodedString");
return HttpUtility.UrlDecode(uriEncodedString);
}
internal static object UtcNow(TemplateContext context, object[] args)
{
var argCount = CountArgs(args);
if (CountArgs(args) > 1)
throw ArgumentsOutOfRange(nameof(UtcNow), args);
var format = "yyyyMMddTHHmmssZ";
if (argCount == 1 && !ExpressionHelpers.TryString(args[0], out format))
throw new ArgumentException(PSRuleResources.FunctionInvalidString, "format");
return DateTime.UtcNow.ToString(format, AzureCulture);
}
internal static object Replace(TemplateContext context, object[] args)
{
if (CountArgs(args) != 3)
@ -1387,7 +1443,6 @@ namespace PSRule.Rules.Azure.Data.Template
throw new ArgumentOutOfRangeException();
string[] delimiter = null;
if (ExpressionHelpers.TryString(args[1], out string single))
{
delimiter = new string[] { single };
@ -1556,6 +1611,22 @@ namespace PSRule.Rules.Azure.Data.Template
);
}
private static ExpressionArgumentException ArgumentInvalidString(string expression, string operand)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidString, operand, expression)
);
}
private static ExpressionArgumentException ArgumentInvalidResourceType(string expression, string providerNamespace, string resourceType)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidResourceType, expression, providerNamespace, resourceType)
);
}
#endregion Exceptions
}
}

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

@ -3,7 +3,9 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace PSRule.Rules.Azure.Data.Template
@ -118,6 +120,25 @@ namespace PSRule.Rules.Azure.Data.Template
public readonly ResourceGroupProperties Properties;
}
public sealed class ResourceProvider
{
internal ResourceProvider()
{
Types = new Dictionary<string, ResourceProviderType>(StringComparer.OrdinalIgnoreCase);
}
public Dictionary<string, ResourceProviderType> Types { get; }
}
public sealed class ResourceProviderType
{
public string ResourceType { get; set; }
public string[] Locations { get; set; }
public string[] ApiVersions { get; set; }
}
public abstract class MockNode
{
protected MockNode(MockNode parent)

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

@ -1,10 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Resources;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace PSRule.Rules.Azure.Data.Template
@ -37,6 +40,7 @@ namespace PSRule.Rules.Azure.Data.Template
{
private readonly Stack<JObject> _Deployment;
private JObject _Parameters;
private readonly Dictionary<string, ResourceProvider> _Providers;
internal TemplateContext()
{
@ -47,6 +51,7 @@ namespace PSRule.Rules.Azure.Data.Template
ResourceGroup = new ResourceGroup();
Subscription = new Subscription();
_Deployment = new Stack<JObject>();
_Providers = ReadProviders();
}
internal TemplateContext(Subscription subscription, ResourceGroup resourceGroup)
@ -230,6 +235,33 @@ namespace PSRule.Rules.Azure.Data.Template
}
return true;
}
private static Dictionary<string, ResourceProvider> ReadProviders()
{
var providersFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "providers.json");
if (!File.Exists(providersFile))
return null;
var json = File.ReadAllText(providersFile);
var settings = new JsonSerializerSettings();
settings.Converters.Add(new ResourceProviderConverter());
var result = JsonConvert.DeserializeObject<Dictionary<string, ResourceProvider>>(json, settings);
return result;
}
internal ResourceProviderType[] GetResourceType(string providerNamespace, string resourceType)
{
if (_Providers == null || _Providers.Count == 0 || !_Providers.TryGetValue(providerNamespace, out ResourceProvider provider))
return Array.Empty<ResourceProviderType>();
if (resourceType == null)
return provider.Types.Values.ToArray();
if (!provider.Types.ContainsKey(resourceType))
return Array.Empty<ResourceProviderType>();
return new ResourceProviderType[] { provider.Types[resourceType] };
}
}
private abstract class LazyValue

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

@ -78,6 +78,24 @@ namespace PSRule.Rules.Azure.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The resource type &apos;{1}/{2}&apos; for &apos;{0}&apos; is not known..
/// </summary>
internal static string ArgumentInvalidResourceType {
get {
return ResourceManager.GetString("ArgumentInvalidResourceType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The argument &apos;{0}&apos; for &apos;{1}&apos; is not a valid string..
/// </summary>
internal static string ArgumentInvalidString {
get {
return ResourceManager.GetString("ArgumentInvalidString", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The number of arguments &apos;{1}&apos; is not within the allowed range for &apos;{0}&apos;..
/// </summary>

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

@ -123,6 +123,12 @@
<data name="ArgumentInvalidInteger" xml:space="preserve">
<value>The argument '{0}' for '{1}' is not a valid integer.</value>
</data>
<data name="ArgumentInvalidResourceType" xml:space="preserve">
<value>The resource type '{1}/{2}' for '{0}' is not known.</value>
</data>
<data name="ArgumentInvalidString" xml:space="preserve">
<value>The argument '{0}' for '{1}' is not a valid string.</value>
</data>
<data name="ArgumentsOutOfRange" xml:space="preserve">
<value>The number of arguments '{1}' is not within the allowed range for '{0}'.</value>
</data>

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

@ -67,38 +67,38 @@ Describe 'Azure Quickstart Templates' -Tag integration {
# Skips templates because they are not complete
$skipTemplates = @(
"100-blank-template"
"101-hdinsight-custom-ambari-db"
"101-sql-vm-aglistener-setup"
"101-sql-vm-ag-setup"
"101-vm-windows-copy-datadisks"
"201-api-management-logs-oms-integration"
"201-data-factory-ftp-hive-blob"
"201-list-storage-keys-windows-vm"
"201-vm-msi"
"201-vm-win-iis-app-ssl"
"201-vmss-win-iis-app-ssl"
"301-custom-images-at-scale"
"301-drupal8-vmss-glusterfs-mysql"
"301-jenkins-aptly-spinnaker-vmss"
"301-vm-sql-full-autobackup-autopatching-keyvault"
"blockchain"
"chef-ha-cluster"
"couchbase"
"hdinsight-genomics-adam"
"hdinsight-linux-run-script-action"
"intel-lustre-client-server"
"sql-server-2014-alwayson-existing-vnet-and-ad"
"kemp-loadmaster-multinic"
"memcached-multi-vm-ubuntu"
"oms-azure-storage-analytics-solution"
"openedx-devstack-ubuntu"
"opensis-cluster-ubuntu"
"rhel-3tier-iaas"
"slurm-on-sles12-hpc"
"traffic-manager-demo-setup"
"vsts-fullbuild-redhat-vm"
"vsts-fullbuild-ubuntu-vm"
"vsts-minbuildjava-ubuntu-vm"
# "101-hdinsight-custom-ambari-db"
# "101-sql-vm-aglistener-setup"
# "101-sql-vm-ag-setup"
# "101-vm-windows-copy-datadisks"
# "201-api-management-logs-oms-integration"
# "201-data-factory-ftp-hive-blob"
# "201-list-storage-keys-windows-vm"
# "201-vm-msi"
# "201-vm-win-iis-app-ssl"
# "201-vmss-win-iis-app-ssl"
# "301-custom-images-at-scale"
# "301-drupal8-vmss-glusterfs-mysql"
# "301-jenkins-aptly-spinnaker-vmss"
# "301-vm-sql-full-autobackup-autopatching-keyvault"
# "blockchain"
# "chef-ha-cluster"
# "couchbase"
# "hdinsight-genomics-adam"
# "hdinsight-linux-run-script-action"
# "intel-lustre-client-server"
# "sql-server-2014-alwayson-existing-vnet-and-ad"
# "kemp-loadmaster-multinic"
# "memcached-multi-vm-ubuntu"
# "oms-azure-storage-analytics-solution"
# "openedx-devstack-ubuntu"
# "opensis-cluster-ubuntu"
# "rhel-3tier-iaas"
# "slurm-on-sles12-hpc"
# "traffic-manager-demo-setup"
# "vsts-fullbuild-redhat-vm"
# "vsts-fullbuild-ubuntu-vm"
# "vsts-minbuildjava-ubuntu-vm"
)
foreach ($sample in (Get-Sample -Path $clonePath)) {
if ($sample.Name -in $skipTemplates) {

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

@ -17,6 +17,7 @@ namespace PSRule.Rules.Azure
private const string TRAIT_ARRAY = "Array";
private const string TRAIT_LOGICAL = "Logical";
private const string TRAIT_COMPARISON = "Comparison";
private const string TRAIT_DATE = "Date";
private const string TRAIT_DEPLOYMENT = "Deployment";
private const string TRAIT_NUMERIC = "Numeric";
private const string TRAIT_STRING = "String";
@ -307,8 +308,8 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.Range(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Range(context, new object[] { }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Range(context, new object[] { 1 }));
Assert.Throws<ArgumentException>(() => Functions.Range(context, new object[] { "one", "two" }));
Assert.Throws<ArgumentException>(() => Functions.Range(context, new object[] { 1, "0" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Range(context, new object[] { "one", "two" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Range(context, new object[] { 1, "0" }));
}
[Fact]
@ -326,7 +327,7 @@ namespace PSRule.Rules.Azure
Assert.Equal("one two three", actual3);
Assert.Throws<ExpressionArgumentException>(() => Functions.Skip(context, new object[] { "one two three" }));
Assert.Throws<ArgumentException>(() => Functions.Skip(context, new object[] { "one two three", "0" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Skip(context, new object[] { "one two three", "0" }));
// Array
var actual4 = Functions.Skip(context, new object[] { JArray.Parse("[ \"one\", \"two\", \"three\" ]"), 2 }) as JArray;
@ -337,8 +338,8 @@ namespace PSRule.Rules.Azure
Assert.Equal(3, actual6.Count);
Assert.Throws<ExpressionArgumentException>(() => Functions.Skip(context, new object[] { JArray.Parse("[ \"one\", \"two\", \"three\" ]") }));
Assert.Throws<ArgumentException>(() => Functions.Skip(context, new object[] { JArray.Parse("[ \"one\", \"two\", \"three\" ]"), "0" }));
Assert.Throws<ArgumentException>(() => Functions.Skip(context, new object[] { 1, "0" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Skip(context, new object[] { JArray.Parse("[ \"one\", \"two\", \"three\" ]"), "0" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Skip(context, new object[] { 1, "0" }));
}
[Fact]
@ -427,6 +428,33 @@ namespace PSRule.Rules.Azure
// TODO: Improve test cases
}
[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void Providers()
{
var context = GetContext();
var actual1 = Functions.Providers(context, new object[] { "Microsoft.Web", "sites" }) as ResourceProviderType;
Assert.NotNull(actual1);
Assert.Equal("sites", actual1.ResourceType);
Assert.Equal("2020-06-01", actual1.ApiVersions[0]);
Assert.Equal("South Central US", actual1.Locations[0]);
var actual2 = Functions.Providers(context, new object[] { "Microsoft.Web" }) as ResourceProviderType[];
Assert.NotNull(actual1);
Assert.Equal(51, actual2.Length);
var actual3 = Functions.Providers(context, new object[] { "microsoft.web", "Sites" }) as ResourceProviderType;
Assert.NotNull(actual3);
Assert.Equal("sites", actual3.ResourceType);
Assert.Throws<ExpressionArgumentException>(() => Functions.Providers(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Providers(context, new object[] { 1 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Providers(context, new object[] { "Microsoft.Web", 1 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Providers(context, new object[] { "Microsoft.Web", "n/a" }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Providers(context, new object[] { "n/a", "n/a" }));
}
[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void Reference()
@ -712,6 +740,46 @@ namespace PSRule.Rules.Azure
#endregion Comparison
#region Date
[Fact]
[Trait(TRAIT, TRAIT_DATE)]
public void DateTimeAdd()
{
var context = GetContext();
var utc = DateTime.Parse("2020-04-07 14:53:14Z", new CultureInfo("en-US"));
var actual1 = DateTime.Parse(Functions.DateTimeAdd(context, new object[] { utc.ToString("u"), "P3Y" }) as string, new CultureInfo("en-US"));
var actual2 = DateTime.Parse(Functions.DateTimeAdd(context, new object[] { utc.ToString("u"), "-P9D" }) as string, new CultureInfo("en-US"));
var actual3 = DateTime.Parse(Functions.DateTimeAdd(context, new object[] { utc.ToString("u"), "PT1H" }) as string, new CultureInfo("en-US"));
Assert.Equal(utc.AddYears(3), actual1.ToUniversalTime());
Assert.Equal(utc.AddDays(-9), actual2.ToUniversalTime());
Assert.Equal(utc.AddHours(1), actual3.ToUniversalTime());
Assert.Throws<ExpressionArgumentException>(() => Functions.DateTimeAdd(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.DateTimeAdd(context, new object[] { }));
Assert.Throws<ExpressionArgumentException>(() => Functions.DateTimeAdd(context, new object[] { 1, 2 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.DateTimeAdd(context, new object[] { utc.ToString("u"), 2 }));
}
[Fact]
[Trait(TRAIT, TRAIT_DATE)]
public void UtcNow()
{
var context = GetContext();
var utc = DateTime.UtcNow;
var actual1 = Functions.UtcNow(context, new object[] { }) as string;
var actual2 = Functions.UtcNow(context, new object[] { "d" }) as string;
var actual3 = Functions.UtcNow(context, new object[] { "M d" }) as string;
Assert.Matches("[0-9]{8}T[0-9]{6}Z", actual1);
Assert.Equal(utc.ToString("d", new CultureInfo("en-US")), actual2);
Assert.Equal(utc.ToString("M d", new CultureInfo("en-US")), actual3);
}
#endregion Date
#region Logical
[Fact]
@ -920,7 +988,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.Base64(context, new object[] { 1 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64(context, new object[] { 1 }));
}
[Fact]
@ -935,7 +1003,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToJson(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToJson(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.Base64ToJson(context, new object[] { 1 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToJson(context, new object[] { 1 }));
}
[Fact]
@ -949,7 +1017,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToString(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToString(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.Base64ToString(context, new object[] { 1 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Base64ToString(context, new object[] { 1 }));
}
[Fact]
@ -1244,7 +1312,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.Uri(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.Uri(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.Uri(context, new object[] { "http://contoso.org/firstpath", 2 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.Uri(context, new object[] { "http://contoso.org/firstpath", 2 }));
}
[Fact]
@ -1258,7 +1326,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponent(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponent(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.UriComponent(context, new object[] { 2 }));
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponent(context, new object[] { 2 }));
}
[Fact]
@ -1272,22 +1340,7 @@ namespace PSRule.Rules.Azure
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponentToString(context, null));
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponentToString(context, new object[] { }));
Assert.Throws<ArgumentException>(() => Functions.UriComponentToString(context, new object[] { 2 }));
}
[Fact]
[Trait(TRAIT, TRAIT_STRING)]
public void UtcNow()
{
var context = GetContext();
var utc = DateTime.UtcNow;
var actual1 = Functions.UtcNow(context, new object[] { }) as string;
var actual2 = Functions.UtcNow(context, new object[] { "d" }) as string;
var actual3 = Functions.UtcNow(context, new object[] { "M d" }) as string;
Assert.Matches("[0-9]{8}T[0-9]{6}Z", actual1);
Assert.Equal(utc.ToString("d", new CultureInfo("en-US")), actual2);
Assert.Equal(utc.ToString("M d", new CultureInfo("en-US")), actual3);
Assert.Throws<ExpressionArgumentException>(() => Functions.UriComponentToString(context, new object[] { 2 }));
}
#endregion String

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

@ -28,6 +28,9 @@
</ItemGroup>
<ItemGroup>
<None Update="providers.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources.Parameters.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Разница между файлами не показана из-за своего большого размера Загрузить разницу