Added support for metadata linking #324 (#325)

This commit is contained in:
Bernie White 2020-03-20 20:48:01 +10:00 коммит произвёл GitHub
Родитель 33f4ea9c6d
Коммит 7eb69f180c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
39 изменённых файлов: 2162 добавлений и 139 удалений

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

@ -2,6 +2,8 @@
## Unreleased
- Added support for linking parameter and template files for analysis with metadata. [#324](https://github.com/Microsoft/PSRule.Rules.Azure/issues/324)
## v0.10.0-B2003032 (pre-release)
- Fixed unused VM resource false positives in templates. [#312](https://github.com/Microsoft/PSRule.Rules.Azure/issues/312)

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

@ -177,6 +177,13 @@ The following commands exist in the `PSRule.Rules.Azure` module:
- [Export-AzRuleData](docs/commands/PSRule.Rules.Azure/en-US/Export-AzRuleData.md) - Export resource configuration data from Azure subscriptions.
- [Export-AzTemplateRuleData](docs/commands/PSRule.Rules.Azure/en-US/Export-AzTemplateRuleData.md) - Export resource configuration data from Azure templates.
- [Get-AzRuleTemplateLink](docs/commands/PSRule.Rules.Azure/en-US/Get-AzRuleTemplateLink.md) - Get a metadata link to a Azure template file.
### Concepts
The following conceptual topics exist in the `PSRule.Rules.Azure` module:
- [Azure metadata link](docs/concepts/PSRule.Rules.Azure/en-US/about_PSRule_Azure_Metadata_Link.md)
## Changes and versioning

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

@ -0,0 +1,112 @@
---
external help file: PSRule.Rules.Azure-help.xml
Module Name: PSRule.Rules.Azure
online version: https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/commands/PSRule.Rules.Azure/en-US/Get-AzRuleTemplateLink.md
schema: 2.0.0
---
# Get-AzRuleTemplateLink
## SYNOPSIS
Get a metadata link to a Azure template file.
## SYNTAX
```text
Get-AzRuleTemplateLink [[-InputPath] <String[]>] [-SkipUnlinked] [[-Path] <String>] [<CommonParameters>]
```
## DESCRIPTION
Gets a link between an Azure Resource Manager (ARM) parameter file and its referenced template file.
Parameter files reference a template file by defining metadata.
By default, when parameter files without a reference to a template are discover and error is raised.
To reference a template, set the `metadata.template` property to a file path.
Referencing templates outside of the path specified with `-Path` is not permitted.
For more information see the [about_PSRule_Azure_Metadata_Link] topic.
## EXAMPLES
### Example 1
```powershell
Get-AzRuleTemplateLink
```
Get links from any `*.parameters.json` files within any folder in the current working path.
## PARAMETERS
### -InputPath
A path or filter to search for parameter files within the path specified by `-Path`.
By default, files with `*.parameters.json` suffix will be used.
When searching for parameter files all sub-directories will be scanned.
To perform a shallow search, prefix input paths with `./`.
```yaml
Type: String[]
Parameter Sets: (All)
Aliases: f, TemplateParameterFile, FullName
Required: False
Position: 1
Default value: '*.parameters.json'
Accept pipeline input: True (ByPropertyName)
Accept wildcard characters: True
```
### -SkipUnlinked
Use this option to ignore parameter files that are not linked to a template.
By default, when a parameter file that does not reference a template is discovered an error will be raised.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Path
Sets the path to search for parameter files in.
By default, this is the current working path.
```yaml
Type: String
Parameter Sets: (All)
Aliases: p
Required: False
Position: 0
Default value: $PWD
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
## INPUTS
### System.String[]
## OUTPUTS
### PSRule.Rules.Azure.Data.Metadata.ITemplateLink
## NOTES
## RELATED LINKS

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

@ -0,0 +1,45 @@
# PSRule_Azure_Metadata_Link
## about_PSRule_Azure_Metadata_Link
## SHORT DESCRIPTION
Describes how Azure Resource Manager (ARM) parameter files reference a template file.
## LONG DESCRIPTION
Azure Resource Manager (ARM) supports storing additional metadata within parameter files.
PSRule uses this metadata to link template and parameter files together to improve unit testing of templates.
To reference a template within a parameter file:
- Set the `metadata.template` property to the template.
- Prefix a template file relative to the parameter file with `./`.
When `./` is not used, the template with is relative to the `-Path` parameter.
For example:
```json
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"template": "./Resources.Template.json"
},
"parameters": {
}
}
```
## NOTE
An online version of this document is available at https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/concepts/PSRule.Rules.Azure/en-US/about_PSRule_Azure_Metadata_Link.md.
## SEE ALSO
- [Get-AzRuleTemplateLink](https://github.com/Microsoft/PSRule.Rules.Azure/blob/master/docs/commands/PSRule.Rules.Azure/en-US/Get-AzRuleTemplateLink.md)
## KEYWORDS
- Link
- Template

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

@ -33,7 +33,7 @@ Name | Synopsis | Severity
[Azure.PostgreSQL.FirewallRuleCount](Azure.PostgreSQL.FirewallRuleCount.md) | Determine if there is an excessive number of firewall rules. | Awareness
[Azure.PublicIP.IsAttached](Azure.PublicIP.IsAttached.md) | Public IP address should be attached. | Awareness
[Azure.Resource.AllowedRegions](Azure.Resource.AllowedRegions.md) | Resources should be deployed to allowed regions. | Awareness
[Azure.Resource.UseTags](Azure.Resource.UseTags.md) | Resources should be tagged. | Awareness
[Azure.Resource.UseTags](Azure.Resource.UseTags.md) | Azure resources should be tagged using a standard convention. | Awareness
[Azure.SQL.FirewallRuleCount](Azure.SQL.FirewallRuleCount.md) | Determine if there is an excessive number of firewall rules. | Awareness
[Azure.VM.Agent](Azure.VM.Agent.md) | Ensure the VM agent is provisioned automatically. | Important
[Azure.VM.NICAttached](Azure.VM.NICAttached.md) | Network interfaces (NICs) should be attached. | Awareness

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

@ -333,7 +333,7 @@ task BuildHelp BuildModule, PlatyPS, {
&$pwshPath -Command {
# Generate MAML and about topics
Import-Module -Name PlatyPS -Verbose:$False;
$Null = New-ExternalHelp -OutputPath 'out/docs/PSRule.Rules.Azure' -Path '.\docs\commands\PSRule.Rules.Azure\en-US' -Force;
$Null = New-ExternalHelp -OutputPath 'out/docs/PSRule.Rules.Azure' -Path '.\docs\commands\PSRule.Rules.Azure\en-US', '.\docs\concepts\PSRule.Rules.Azure\en-US' -Force;
# Copy generated help into module out path
$Null = Copy-Item -Path out/docs/PSRule.Rules.Azure/* -Destination out/modules/PSRule.Rules.Azure/en-US/ -Recurse;

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

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace PSRule.Rules.Azure.Configuration
{
[JsonConverter(typeof(StringEnumConverter))]
public enum OutputEncoding
{
Default = 0,
UTF8 = 1,
UTF7 = 2,
Unicode = 3,
UTF32 = 4,
ASCII = 5
}
}

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

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.ComponentModel;
namespace PSRule.Rules.Azure.Configuration
{
/// <summary>
/// Options for generating and formatting output.
/// </summary>
public sealed class OutputOption : IEquatable<OutputOption>
{
private const OutputEncoding DEFAULT_ENCODING = OutputEncoding.Default;
internal static readonly OutputOption Default = new OutputOption
{
Encoding = DEFAULT_ENCODING
};
public OutputOption()
{
Encoding = null;
Path = null;
}
public OutputOption(OutputOption option)
{
if (option == null)
throw new ArgumentNullException(nameof(option));
Encoding = option.Encoding;
Path = option.Path;
}
public override bool Equals(object obj)
{
return obj is OutputOption option && Equals(option);
}
public bool Equals(OutputOption other)
{
return other != null &&
Encoding == other.Encoding &&
Path == other.Path;
}
public override int GetHashCode()
{
unchecked // Overflow is fine
{
int hash = 17;
hash = hash * 23 + (Encoding.HasValue ? Encoding.Value.GetHashCode() : 0);
hash = hash * 23 + (Path != null ? Path.GetHashCode() : 0);
return hash;
}
}
/// <summary>
/// The encoding to use when writing results to file.
/// </summary>
[DefaultValue(null)]
public OutputEncoding? Encoding { get; set; }
/// <summary>
/// The file path location to save results.
/// </summary>
[DefaultValue(null)]
public string Path { get; set; }
}
}

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

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
using System.IO;
using System.Management.Automation;
@ -13,11 +14,27 @@ namespace PSRule.Rules.Azure.Configuration
public sealed class PSRuleOption
{
internal static readonly PSRuleOption Default = new PSRuleOption
{
Output = OutputOption.Default
};
/// <summary>
/// A callback that is overridden by PowerShell so that the current working path can be retrieved.
/// </summary>
private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory();
public PSRuleOption()
{
// Set defaults
Output = new OutputOption();
}
/// <summary>
/// Options that affect how output is generated.
/// </summary>
public OutputOption Output { get; set; }
/// <summary>
/// Set working path from PowerShell host environment.
/// </summary>
@ -30,10 +47,8 @@ namespace PSRule.Rules.Azure.Configuration
if (executionContext == null)
{
_GetWorkingPath = () => Directory.GetCurrentDirectory();
return;
}
_GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path;
}
@ -45,12 +60,27 @@ namespace PSRule.Rules.Azure.Configuration
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal static string GetRootedPath(string path)
{
return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path));
}
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
internal static string GetRootedBasePath(string path)
{
var rootedPath = GetRootedPath(path);
if (rootedPath.Length > 0 && IsSeparator(rootedPath[rootedPath.Length - 1]))
return rootedPath;
return string.Concat(rootedPath, Path.DirectorySeparatorChar);
}
[DebuggerStepThrough]
private static bool IsSeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}
}
}

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

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Metadata
{
public interface ITemplateLink
{
string TemplateFile { get; }
string ParameterFile { get; }
}
}

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

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Data.Metadata
{
internal sealed class TemplateLink : ITemplateLink
{
internal TemplateLink(string templateFile, string parameterFile)
{
TemplateFile = templateFile;
ParameterFile = parameterFile;
}
public string Name { get; internal set; }
public string Description { get; internal set; }
public string TemplateFile { get; }
public string ParameterFile { get; }
}
}

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

@ -34,6 +34,11 @@ This project is to be considered a proof-of-concept and not a supported product.
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Diagnostics.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Diagnostics.resx</DependentUpon>
</Compile>
<Compile Update="Resources\PSRuleResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -42,6 +47,10 @@ This project is to be considered a proof-of-concept and not a supported product.
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Diagnostics.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Diagnostics.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\PSRuleResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>PSRuleResources.Designer.cs</LastGenOutput>

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

@ -78,6 +78,7 @@ RequiredAssemblies = @(
FunctionsToExport = @(
'Export-AzRuleData'
'Export-AzTemplateRuleData'
'Get-AzRuleTemplateLink'
)
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.

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

@ -121,11 +121,13 @@ function Export-AzTemplateRuleData {
begin {
Write-Verbose -Message '[Export-AzTemplateRuleData] BEGIN::';
$Option = [PSRule.Rules.Azure.Configuration.PSRuleOption]::new();
$Option.Output.Path = $OutputPath;
# Build the pipeline
$builder = [PSRule.Rules.Azure.Pipeline.PipelineBuilder]::Template();
$builder = [PSRule.Rules.Azure.Pipeline.PipelineBuilder]::Template($Option);
$builder.Deployment($Name);
$builder.PassThru($PassThru);
$builder.OutputPath($OutputPath);
# Bind to subscription context
if ($PSBoundParameters.ContainsKey('Subscription')) {
@ -142,7 +144,7 @@ function Export-AzTemplateRuleData {
}
}
$builder.UseCommandRuntime($PSCmdlet.CommandRuntime);
$builder.UseCommandRuntime($PSCmdlet);
$builder.UseExecutionContext($ExecutionContext);
try {
$pipeline = $builder.Build();
@ -177,6 +179,68 @@ function Export-AzTemplateRuleData {
}
}
# .ExternalHelp PSRule.Rules.Azure-help.xml
function Get-AzRuleTemplateLink {
[CmdletBinding()]
[OutputType([PSRule.Rules.Azure.Data.Metadata.ITemplateLink])]
param (
[Parameter(Position = 1, Mandatory = $False, ValueFromPipelineByPropertyName = $True)]
[Alias('f', 'TemplateParameterFile', 'FullName')]
[SupportsWildcards()]
[String[]]$InputPath = '*.parameters.json',
[Parameter(Mandatory = $False)]
[Switch]$SkipUnlinked,
[Parameter(Position = 0, Mandatory = $False)]
[Alias('p')]
[String]$Path = $PWD
)
begin {
Write-Verbose -Message '[Get-AzRuleTemplateLink] BEGIN::';
# Build the pipeline
$builder = [PSRule.Rules.Azure.Pipeline.PipelineBuilder]::TemplateLink($Path);
$builder.SkipUnlinked($SkipUnlinked);
$builder.UseCommandRuntime($PSCmdlet);
$builder.UseExecutionContext($ExecutionContext);
$pipeline = $builder.Build();
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
try {
$pipeline.Begin();
}
catch {
$pipeline.Dispose();
throw;
}
}
}
process {
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
try {
foreach ($p in $InputPath) {
$pipeline.Process($p);
}
}
catch {
$pipeline.Dispose();
throw;
}
}
}
end {
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
try {
$pipeline.End();
}
finally {
$pipeline.Dispose();
}
}
Write-Verbose -Message '[Get-AzRuleTemplateLink] END::';
}
}
#endregion Public functions
#
@ -811,4 +875,8 @@ function SetResourceType {
# Export module
#
Export-ModuleMember -Function 'Export-AzRuleData', 'Export-AzTemplateRuleData';
Export-ModuleMember -Function @(
'Export-AzRuleData'
'Export-AzTemplateRuleData'
'Get-AzRuleTemplateLink'
);

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

@ -23,23 +23,19 @@ namespace PSRule.Rules.Azure.Pipeline
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
protected PipelineException(string message) : base(message)
{
}
protected PipelineException(string message)
: base(message) { }
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
protected PipelineException(string message, Exception innerException) : base(message, innerException)
{
}
protected PipelineException(string message, Exception innerException)
: base(message, innerException) { }
protected PipelineException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
: base(info, context) { }
}
/// <summary>
@ -59,22 +55,60 @@ namespace PSRule.Rules.Azure.Pipeline
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public PipelineSerializationException(string message) : base(message)
{
}
public PipelineSerializationException(string message)
: base(message) { }
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
public PipelineSerializationException(string message, Exception innerException) : base(message, innerException)
public PipelineSerializationException(string message, Exception innerException)
: base(message, innerException) { }
private PipelineSerializationException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
base.GetObjectData(info, context);
}
}
/// <summary>
/// An exception related to template linking.
/// </summary>
[Serializable]
public sealed class InvalidTemplateLinkException : PipelineException
{
/// <summary>
/// Creates a template linking exception.
/// </summary>
public InvalidTemplateLinkException()
{
}
private PipelineSerializationException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public InvalidTemplateLinkException(string message)
: base(message) { }
/// <summary>
/// Creates a template linking exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
public InvalidTemplateLinkException(string message, Exception innerException)
: base(message, innerException) { }
private InvalidTemplateLinkException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)

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

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Resources;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// Extensions for logging to the pipeline.
/// </summary>
internal static class LoggingExtensions
{
internal static void VerboseFindFiles(this ILogger logger, string path)
{
logger.WriteVerbose(Diagnostics.VerboseFindFiles, path);
}
internal static void VerboseFoundFile(this ILogger logger, string path)
{
logger.WriteVerbose(Diagnostics.VerboseFoundFile, path);
}
internal static void VerboseMetadataNotFound(this ILogger logger, string path)
{
logger.WriteVerbose(Diagnostics.VerboseMetadataNotFound, path);
}
internal static void VerboseTemplateLinkNotFound(this ILogger logger, string path)
{
logger.WriteVerbose(Diagnostics.VerboseTemplateLinkNotFound, path);
}
internal static void VerboseTemplateFileNotFound(this ILogger logger, string path)
{
logger.WriteVerbose(Diagnostics.VerboseTemplateFileNotFound, path);
}
}
}

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

@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Resources;
using System.IO;
using System.Text;
namespace PSRule.Rules.Azure.Pipeline.Output
{
/// <summary>
/// An output writer that writes output to disk.
/// </summary>
internal sealed class FileOutputWriter : PipelineWriter
{
private readonly Encoding _Encoding;
private readonly string _Path;
private readonly string _DefaultFile;
private readonly ShouldProcess _ShouldProcess;
internal FileOutputWriter(PipelineWriter inner, PSRuleOption option, Encoding encoding, string path, string defaultFile, ShouldProcess shouldProcess)
: base(inner, option)
{
_Encoding = encoding;
_Path = path;
_DefaultFile = defaultFile;
_ShouldProcess = shouldProcess;
}
public override void WriteObject(object sendToPipeline, bool enumerateCollection)
{
WriteToFile(sendToPipeline);
}
private void WriteToFile(object o)
{
var rootedPath = PSRuleOption.GetRootedPath(_Path);
if (!Path.HasExtension(rootedPath) || Directory.Exists(rootedPath))
rootedPath = Path.Combine(rootedPath, _DefaultFile);
var parentPath = Directory.GetParent(rootedPath);
if (!parentPath.Exists && _ShouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath))
Directory.CreateDirectory(path: parentPath.FullName);
if (_ShouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile))
{
File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: _Encoding);
var info = new FileInfo(rootedPath);
base.WriteObject(info, false);
}
}
}
}

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

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using PSRule.Rules.Azure.Configuration;
namespace PSRule.Rules.Azure.Pipeline.Output
{
internal sealed class JsonOutputWriter : SerializationOutputWriter<object>
{
internal JsonOutputWriter(PipelineWriter inner, PSRuleOption option)
: base(inner, option) { }
protected override string Serialize(object[] o)
{
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
settings.Converters.Add(new PSObjectJsonConverter());
return JsonConvert.SerializeObject(o, settings: settings);
}
}
}

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

@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Configuration;
using System;
using System.Collections.Generic;
using System.Management.Automation;
namespace PSRule.Rules.Azure.Pipeline.Output
{
/// <summary>
/// An output writer that returns output to the host PowerShell runspace.
/// </summary>
internal sealed class PSPipelineWriter : PipelineWriter
{
private const string Source = "PSRule";
private const string HostTag = "PSHOST";
private Action<string> OnWriteWarning;
private Action<string> OnWriteVerbose;
private Action<ErrorRecord> OnWriteError;
private Action<InformationRecord> OnWriteInformation;
private Action<string> OnWriteDebug;
internal Action<object, bool> OnWriteObject;
private bool _LogError;
private bool _LogWarning;
private bool _LogVerbose;
private bool _LogInformation;
private bool _LogDebug;
internal PSPipelineWriter(PSRuleOption option)
: base(null, option) { }
internal void UseCommandRuntime(PSCmdlet commandRuntime)
{
if (commandRuntime == null)
return;
OnWriteVerbose = commandRuntime.WriteVerbose;
OnWriteWarning = commandRuntime.WriteWarning;
OnWriteError = commandRuntime.WriteError;
OnWriteInformation = commandRuntime.WriteInformation;
OnWriteDebug = commandRuntime.WriteDebug;
OnWriteObject = commandRuntime.WriteObject;
}
internal void UseExecutionContext(EngineIntrinsics executionContext)
{
if (executionContext == null)
return;
_LogError = GetPreferenceVariable(executionContext, ErrorPreference);
_LogWarning = GetPreferenceVariable(executionContext, WarningPreference);
_LogVerbose = GetPreferenceVariable(executionContext, VerbosePreference);
_LogInformation = GetPreferenceVariable(executionContext, InformationPreference);
_LogDebug = GetPreferenceVariable(executionContext, DebugPreference);
}
private static bool GetPreferenceVariable(EngineIntrinsics executionContext, string variableName)
{
var preference = GetPreferenceVariable(executionContext.SessionState, variableName);
if (preference == ActionPreference.Ignore)
return false;
return !(preference == ActionPreference.SilentlyContinue && (
variableName == VerbosePreference ||
variableName == DebugPreference)
);
}
#region Internal logging methods
/// <summary>
/// Core methods to hand off to logger.
/// </summary>
/// <param name="errorRecord">A valid PowerShell error record.</param>
public override void WriteError(ErrorRecord errorRecord)
{
if (OnWriteError == null || !ShouldWriteError())
return;
OnWriteError(errorRecord);
}
/// <summary>
/// Core method to hand off verbose messages to logger.
/// </summary>
/// <param name="message">A message to log.</param>
public override void WriteVerbose(string message)
{
if (OnWriteVerbose == null || !ShouldWriteVerbose())
return;
OnWriteVerbose(message);
}
/// <summary>
/// Core method to hand off warning messages to logger.
/// </summary>
/// <param name="message">A message to log</param>
public override void WriteWarning(string message)
{
if (OnWriteWarning == null || !ShouldWriteWarning())
return;
OnWriteWarning(message);
}
/// <summary>
/// Core method to hand off information messages to logger.
/// </summary>
public override void WriteInformation(InformationRecord informationRecord)
{
if (OnWriteInformation == null || !ShouldWriteInformation())
return;
OnWriteInformation(informationRecord);
}
/// <summary>
/// Core method to hand off debug messages to logger.
/// </summary>
public override void WriteDebug(DebugRecord debugRecord)
{
if (OnWriteDebug == null || !ShouldWriteDebug())
return;
OnWriteDebug(debugRecord.Message);
}
public override void WriteObject(object sendToPipeline, bool enumerateCollection)
{
if (OnWriteObject == null)
return;
OnWriteObject(sendToPipeline, enumerateCollection);
}
public override void WriteHost(HostInformationMessage info)
{
if (OnWriteInformation == null)
return;
var record = new InformationRecord(info, Source);
record.Tags.Add(HostTag);
OnWriteInformation(record);
}
public override bool ShouldWriteVerbose()
{
return _LogVerbose;
}
public override bool ShouldWriteInformation()
{
return _LogInformation;
}
public override bool ShouldWriteDebug()
{
return _LogDebug;
}
public override bool ShouldWriteError()
{
return _LogError;
}
public override bool ShouldWriteWarning()
{
return _LogWarning;
}
#endregion Internal logging methods
}
}

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

@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Configuration;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A helper to build a list of rule sources for discovery.
/// </summary>
internal sealed class PathBuilder
{
// Path separators
private const char Slash = '/';
private const char BackSlash = '\\';
private const char Dot = '.';
private static readonly char[] PathLiteralStopCharacters = new char[] { '*', '[', '?' };
private static readonly char[] PathSeparatorCharacters = new char[] { '\\', '/' };
private readonly ILogger Logger;
private readonly List<FileInfo> _Source;
private readonly string _BasePath;
private readonly string _DefaultSearchPattern;
internal PathBuilder(ILogger logger, string basePath, string searchPattern)
{
Logger = logger;
_Source = new List<FileInfo>();
_BasePath = PSRuleOption.GetRootedBasePath(basePath);
_DefaultSearchPattern = searchPattern;
}
public void Add(string[] path)
{
if (path == null || path.Length == 0)
return;
for (var i = 0; i < path.Length; i++)
Add(path[i]);
}
public void Add(string path)
{
if (string.IsNullOrEmpty(path))
return;
FindFiles(path);
}
public FileInfo[] Build()
{
try
{
return _Source.ToArray();
}
finally
{
_Source.Clear();
}
}
private void FindFiles(string path)
{
Logger.VerboseFindFiles(path);
if (TryAddFile(path))
return;
var pathLiteral = GetSearchParameters(path, out string searchPattern, out SearchOption searchOption);
var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption);
foreach (var file in files)
AddFile(file);
}
private bool TryAddFile(string path)
{
if (path.IndexOfAny(PathLiteralStopCharacters) > -1)
return false;
var rootedPath = GetRootedPath(path);
if (!File.Exists(rootedPath))
return false;
AddFile(rootedPath);
return true;
}
private void AddFile(string path)
{
Logger.VerboseFoundFile(path);
_Source.Add(new FileInfo(path));
}
private string GetRootedPath(string path)
{
return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(_BasePath, path));
}
/// <summary>
/// Split a search path into components based on wildcards.
/// </summary>
private string GetSearchParameters(string path, out string searchPattern, out SearchOption searchOption)
{
searchOption = SearchOption.AllDirectories;
var pathLiteral = SplitSearchPath(TrimPath(path, out bool relativeAnchor), out searchPattern);
if (string.IsNullOrEmpty(searchPattern))
searchPattern = _DefaultSearchPattern;
// If a path separator is within the pattern use a resursive search
if (relativeAnchor || !string.IsNullOrEmpty(pathLiteral))
searchOption = SearchOption.TopDirectoryOnly;
return GetRootedPath(pathLiteral);
}
private static string SplitSearchPath(string path, out string searchPattern)
{
// Find the index of the first expression character i.e. out/modules/**/file
var stopIndex = path.IndexOfAny(PathLiteralStopCharacters);
// Track back to the separator before any expression characters
var literalSeparator = stopIndex > -1 ? path.LastIndexOfAny(PathSeparatorCharacters, stopIndex) + 1 : path.LastIndexOfAny(PathSeparatorCharacters) + 1;
searchPattern = path.Substring(literalSeparator, path.Length - literalSeparator);
return path.Substring(0, literalSeparator);
}
/// <summary>
/// Remove leading ./ or .\ characters indicating a relative path anchor.
/// </summary>
/// <param name="path">The path to trim.</param>
/// <param name="relativeAnchor">Returns true when a relative path anchor was present.</param>
/// <returns>Return a clean path without the relative path anchor.</returns>
private static string TrimPath(string path, out bool relativeAnchor)
{
relativeAnchor = false;
if (path.Length >= 2 && path[0] == Dot && IsSeparator(path[1]))
{
relativeAnchor = true;
return path.Remove(0, 2);
}
return path;
}
[DebuggerStepThrough]
private static bool IsSeparator(char c)
{
return c == Slash || c == BackSlash;
}
}
}

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

@ -2,11 +2,9 @@
// Licensed under the MIT License.
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Resources;
using PSRule.Rules.Azure.Pipeline.Output;
using System;
using System.IO;
using System.Management.Automation;
using System.Text;
namespace PSRule.Rules.Azure.Pipeline
{
@ -17,15 +15,20 @@ namespace PSRule.Rules.Azure.Pipeline
/// </summary>
public static class PipelineBuilder
{
public static ITemplatePipelineBuilder Template()
public static ITemplatePipelineBuilder Template(PSRuleOption option)
{
return new TemplatePipelineBuilder();
return new TemplatePipelineBuilder(option);
}
public static ITemplateLinkPipelineBuilder TemplateLink(string path)
{
return new TemplateLinkPipelineBuilder(path);
}
}
public interface IPipelineBuilder
{
void UseCommandRuntime(ICommandRuntime2 commandRuntime);
void UseCommandRuntime(PSCmdlet commandRuntime);
void UseExecutionContext(EngineIntrinsics executionContext);
@ -45,30 +48,37 @@ namespace PSRule.Rules.Azure.Pipeline
internal abstract class PipelineBuilderBase : IPipelineBuilder
{
private readonly PSPipelineWriter _Output;
protected readonly PSRuleOption Option;
protected WriteOutput Output;
protected ShouldProcess ShouldProcess;
protected PSCmdlet CmdletContext;
protected EngineIntrinsics ExecutionContext;
protected PipelineBuilderBase()
{
Option = new PSRuleOption();
_Output = new PSPipelineWriter(Option);
}
public virtual void UseCommandRuntime(ICommandRuntime2 commandRuntime)
public virtual void UseCommandRuntime(PSCmdlet commandRuntime)
{
//Logger.UseCommandRuntime(commandRuntime);
Output = commandRuntime.WriteObject;
ShouldProcess = commandRuntime.ShouldProcess;
CmdletContext = commandRuntime;
_Output.UseCommandRuntime(commandRuntime);
}
public void UseExecutionContext(EngineIntrinsics executionContext)
{
//Logger.UseExecutionContext(executionContext);
ExecutionContext = executionContext;
_Output.UseExecutionContext(executionContext);
}
public virtual IPipelineBuilder Configure(PSRuleOption option)
{
if (option == null)
return this;
Option.Output = new OutputOption(option.Output);
return this;
}
@ -81,47 +91,31 @@ namespace PSRule.Rules.Azure.Pipeline
protected virtual PipelineWriter PrepareWriter()
{
return new PowerShellWriter(Output);
var writer = new PSPipelineWriter(Option);
writer.UseCommandRuntime(CmdletContext);
writer.UseExecutionContext(ExecutionContext);
return writer;
}
/// <summary>
/// Write output to file.
/// </summary>
/// <param name="path">The file path to write.</param>
/// <param name="defaultFile">The default file name to use when a directory is specified.</param>
/// <param name="encoding">The file encoding to use.</param>
/// <param name="o">The text to write.</param>
protected static void WriteToFile(string path, string defaultFile, ShouldProcess shouldProcess, WriteOutput output, Encoding encoding, object o)
protected virtual PipelineWriter GetOutput()
{
var rootedPath = PSRuleOption.GetRootedPath(path: path);
if (!Path.HasExtension(rootedPath) || Directory.Exists(rootedPath))
rootedPath = Path.Combine(rootedPath, defaultFile);
var parentPath = Directory.GetParent(rootedPath);
if (!parentPath.Exists && shouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath))
{
Directory.CreateDirectory(path: parentPath.FullName);
}
if (shouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile))
{
File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: encoding);
var info = new FileInfo(rootedPath);
output(info, false);
}
return _Output;
}
}
internal abstract class PipelineBase : IDisposable, IPipeline
{
protected readonly PipelineContext Context;
protected readonly PipelineWriter Writer;
// Track whether Dispose has been called.
private bool _Disposed = false;
protected PipelineBase(PipelineContext context)
protected PipelineBase(PipelineContext context, PipelineWriter writer)
{
Context = context;
Writer = writer;
}
#region IPipeline
@ -138,7 +132,7 @@ namespace PSRule.Rules.Azure.Pipeline
public virtual void End()
{
//Writer.End();
Writer.End();
}
#endregion IPipeline

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

@ -1,78 +1,187 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using PSRule.Rules.Azure.Configuration;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Threading;
namespace PSRule.Rules.Azure.Pipeline
{
internal delegate void WriteOutput(object o, bool enumerate);
internal abstract class PipelineWriter
internal interface ILogger
{
private readonly WriteOutput _Output;
void WriteVerbose(string message);
protected PipelineWriter(WriteOutput output)
void WriteVerbose(string format, params object[] args);
}
internal abstract class PipelineWriter : ILogger
{
protected const string ErrorPreference = "ErrorActionPreference";
protected const string WarningPreference = "WarningPreference";
protected const string VerbosePreference = "VerbosePreference";
protected const string InformationPreference = "InformationPreference";
protected const string DebugPreference = "DebugPreference";
private readonly PipelineWriter _Writer;
protected readonly PSRuleOption Option;
protected PipelineWriter(PipelineWriter inner, PSRuleOption option)
{
_Output = output;
_Writer = inner;
Option = option;
}
public virtual void Write(object o, bool enumerate)
public virtual void Begin()
{
_Output(o, enumerate);
if (_Writer == null)
return;
_Writer.Begin();
}
public virtual void WriteObject(object sendToPipeline, bool enumerateCollection)
{
if (_Writer == null || sendToPipeline == null)
return;
_Writer.WriteObject(sendToPipeline, enumerateCollection);
}
public virtual void End()
{
// Do nothing
}
}
internal sealed class PowerShellWriter : PipelineWriter
{
internal PowerShellWriter(WriteOutput output)
: base(output) { }
public override void Write(object o, bool enumerate)
{
base.Write(o, enumerate);
}
}
internal sealed class JsonPipelineWriter : PipelineWriter
{
private readonly List<object> _Result;
internal JsonPipelineWriter(WriteOutput output)
: base(output)
{
_Result = new List<object>();
}
public override void Write(object o, bool enumerate)
{
if (enumerate && o is IEnumerable<object> items)
{
_Result.AddRange(items);
if (_Writer == null)
return;
}
_Result.Add(o);
_Writer.End();
}
public override void End()
public void WriteVerbose(string format, params object[] args)
{
WriteObjectJson();
if (!ShouldWriteVerbose())
return;
WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args));
}
private void WriteObjectJson()
public virtual void WriteVerbose(string message)
{
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
settings.Converters.Add(new PSObjectJsonConverter());
var json = JsonConvert.SerializeObject(_Result.ToArray(), settings: settings);
base.Write(json, false);
if (_Writer == null || string.IsNullOrEmpty(message))
return;
_Writer.WriteVerbose(message);
}
public virtual bool ShouldWriteVerbose()
{
return _Writer != null && _Writer.ShouldWriteVerbose();
}
public virtual void WriteWarning(string message)
{
if (_Writer == null || string.IsNullOrEmpty(message))
return;
_Writer.WriteWarning(message);
}
public virtual bool ShouldWriteWarning()
{
return _Writer != null && _Writer.ShouldWriteWarning();
}
public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject)
{
if (!ShouldWriteError())
return;
WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject));
}
public virtual void WriteError(ErrorRecord errorRecord)
{
if (_Writer == null || errorRecord == null)
return;
_Writer.WriteError(errorRecord);
}
public virtual bool ShouldWriteError()
{
return _Writer != null && _Writer.ShouldWriteError();
}
public virtual void WriteInformation(InformationRecord informationRecord)
{
if (_Writer == null || informationRecord == null)
return;
_Writer.WriteInformation(informationRecord);
}
public virtual void WriteHost(HostInformationMessage info)
{
if (_Writer == null)
return;
_Writer.WriteHost(info);
}
public virtual bool ShouldWriteInformation()
{
return _Writer != null && _Writer.ShouldWriteInformation();
}
public virtual void WriteDebug(DebugRecord debugRecord)
{
if (_Writer == null || debugRecord == null)
return;
_Writer.WriteDebug(debugRecord);
}
public virtual bool ShouldWriteDebug()
{
return _Writer != null && _Writer.ShouldWriteDebug();
}
protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName)
{
return (ActionPreference)sessionState.PSVariable.GetValue(variableName);
}
}
internal abstract class SerializationOutputWriter<T> : PipelineWriter
{
private readonly List<T> _Result;
protected SerializationOutputWriter(PipelineWriter inner, PSRuleOption option)
: base(inner, option)
{
_Result = new List<T>();
}
public override void WriteObject(object sendToPipeline, bool enumerateCollection)
{
Add(sendToPipeline);
}
protected void Add(object o)
{
if (o is T[] collection)
_Result.AddRange(collection);
else if (o is T item)
_Result.Add(item);
}
public sealed override void End()
{
var results = _Result.ToArray();
base.WriteObject(Serialize(results), false);
}
protected abstract string Serialize(T[] o);
}
}

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

@ -0,0 +1,236 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data.Metadata;
using PSRule.Rules.Azure.Resources;
using System;
using System.Globalization;
using System.IO;
using System.Management.Automation;
namespace PSRule.Rules.Azure.Pipeline
{
public interface ITemplateLinkPipelineBuilder : IPipelineBuilder
{
void SkipUnlinked(bool skipUnlinked);
}
internal sealed class TemplateLinkPipelineBuilder : PipelineBuilderBase, ITemplateLinkPipelineBuilder
{
private readonly string _BasePath;
private bool _SkipUnlinked;
internal TemplateLinkPipelineBuilder(string basePath)
{
_BasePath = basePath;
}
public void SkipUnlinked(bool skipUnlinked)
{
_SkipUnlinked = skipUnlinked;
}
public override IPipeline Build()
{
return new TemplateLinkPipeline(PrepareContext(), PrepareWriter(), _BasePath, _SkipUnlinked);
}
}
internal sealed class TemplateLinkPipeline : PipelineBase
{
private const string PROPERTYNAME_SCHEMA = "$schema";
private const string PROPERTYNAME_METADATA = "metadata";
private const string PROPERTYNAME_TEMPLATE = "template";
private const string PROPERTYNAME_NAME = "name";
private const string PROPERTYNAME_DESCRIPTION = "description";
private const string DEFAULT_TEMPLATESEARCH_PATTERN = "*.parameters.json";
private readonly string _BasePath;
private readonly bool _SkipUnlinked;
private readonly PathBuilder _PathBuilder;
internal TemplateLinkPipeline(PipelineContext context, PipelineWriter writer, string basePath, bool skipUnlinked)
: base(context, writer)
{
_BasePath = PSRuleOption.GetRootedBasePath(basePath);
_SkipUnlinked = skipUnlinked;
_PathBuilder = new PathBuilder(writer, basePath, DEFAULT_TEMPLATESEARCH_PATTERN);
}
public override void Process(PSObject sourceObject)
{
if (sourceObject == null || !GetPath(sourceObject, out string path))
return;
_PathBuilder.Add(path);
var fileInfos = _PathBuilder.Build();
foreach (var info in fileInfos)
ProcessParameterFile(info.FullName);
}
private void ProcessParameterFile(string parameterFile)
{
try
{
var rootedParameterFile = PSRuleOption.GetRootedPath(parameterFile);
// Check if metadata property exists
if (!TryMetadata(rootedParameterFile, out JObject metadata))
return;
if (!TryTemplateFile(metadata, rootedParameterFile, out string templateFile))
return;
var templateLink = new TemplateLink(templateFile, rootedParameterFile);
// Populate remaining properties
if (TryStringProperty(metadata, PROPERTYNAME_NAME, out string name))
templateLink.Name = name;
if (TryStringProperty(metadata, PROPERTYNAME_DESCRIPTION, out string description))
templateLink.Description = description;
Writer.WriteObject(templateLink, false);
}
catch (InvalidOperationException ex)
{
Writer.WriteError(ex, nameof(InvalidOperationException), ErrorCategory.InvalidOperation, parameterFile);
}
catch (FileNotFoundException ex)
{
Writer.WriteError(ex, nameof(FileNotFoundException), ErrorCategory.ObjectNotFound, parameterFile);
}
catch (PipelineException ex)
{
Writer.WriteError(ex, nameof(PipelineException), ErrorCategory.WriteError, parameterFile);
}
}
private static bool GetPath(PSObject sourceObject, out string path)
{
path = null;
if (sourceObject.BaseObject is string s)
{
path = s;
return true;
}
if (sourceObject.BaseObject is FileInfo info && info.Extension == ".json")
{
path = info.FullName;
return true;
}
return false;
}
private static T ReadFile<T>(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
throw new FileNotFoundException(string.Format(CultureInfo.CurrentCulture, PSRuleResources.ParameterFileNotFound, path), path);
try
{
return JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
}
catch (InvalidCastException)
{
// Discard exception
return default(T);
}
}
/// <summary>
/// Check the JSON object is an ARM template parameter file.
/// </summary>
private static bool IsParameterFile(JObject value)
{
if (!value.TryGetValue(PROPERTYNAME_SCHEMA, out JToken token) || !Uri.TryCreate(token.Value<string>(), UriKind.Absolute, out Uri schemaUri))
return false;
return StringComparer.OrdinalIgnoreCase.Equals(schemaUri.Host, "schema.management.azure.com") &&
StringComparer.OrdinalIgnoreCase.Equals(schemaUri.PathAndQuery, "/schemas/2015-01-01/deploymentParameters.json");
}
private bool TryMetadata(string parameterFile, out JObject metadata)
{
var parameterObject = ReadFile<JObject>(parameterFile);
metadata = null;
// Check that the JSON file is an ARM template parameter file
if (parameterObject == null || !IsParameterFile(parameterObject))
return false;
if (parameterObject.TryGetValue(PROPERTYNAME_METADATA, out JToken metadataToken) && metadataToken is JObject property)
{
metadata = property;
return true;
}
Writer.VerboseMetadataNotFound(parameterFile);
if (!_SkipUnlinked)
throw new InvalidTemplateLinkException(string.Format(CultureInfo.CurrentCulture, PSRuleResources.MetadataNotFound, parameterFile));
return false;
}
private bool TryTemplateFile(JObject metadata, string parameterFile, out string templateFile)
{
if (!(TryStringProperty(metadata, PROPERTYNAME_TEMPLATE, out templateFile)))
{
if (_SkipUnlinked)
{
Writer.VerboseTemplateLinkNotFound(parameterFile);
return false;
}
else throw new InvalidTemplateLinkException(string.Format(CultureInfo.CurrentCulture, PSRuleResources.TemplateLinkNotFound, parameterFile));
}
templateFile = TrimSlash(templateFile);
var pathBase = IsRelative(templateFile) ? Path.GetDirectoryName(parameterFile) : PSRuleOption.GetWorkingPath();
templateFile = Path.GetFullPath(Path.Combine(pathBase, templateFile));
// Template file must be within working path
if (!templateFile.StartsWith(PSRuleOption.GetRootedBasePath(""), StringComparison.Ordinal))
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, PSRuleResources.PathTraversal, templateFile));
if (!File.Exists(templateFile))
{
Writer.VerboseTemplateFileNotFound(templateFile);
throw new FileNotFoundException(
string.Format(CultureInfo.CurrentCulture, PSRuleResources.TemplateFileReferenceNotFound, parameterFile),
new FileNotFoundException(string.Format(CultureInfo.CurrentCulture, PSRuleResources.TemplateFileNotFound, templateFile))
);
}
return true;
}
private static bool TryStringProperty(JObject o, string propertyName, out string value)
{
value = null;
return o.TryGetValue(propertyName, out JToken token) && TryString(token, out value);
}
private static bool TryString(JToken token, out string value)
{
value = null;
if (token == null || token.Type != JTokenType.String)
return false;
value = token.Value<string>();
return true;
}
private static bool IsRelative(string path)
{
return path.StartsWith("./", StringComparison.OrdinalIgnoreCase) || path.StartsWith("../", StringComparison.OrdinalIgnoreCase);
}
private static string TrimSlash(string path)
{
return string.IsNullOrEmpty(path) || path[0] != '/' ? path : path.TrimStart('/');
}
}
}

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

@ -5,6 +5,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data.Template;
using PSRule.Rules.Azure.Pipeline.Output;
using PSRule.Rules.Azure.Resources;
using System;
using System.Collections;
@ -25,8 +26,6 @@ namespace PSRule.Rules.Azure.Pipeline
void Subscription(PSObject subscription);
void PassThru(bool passThru);
void OutputPath(string outputPath);
}
internal sealed class TemplatePipelineBuilder : PipelineBuilderBase, ITemplatePipelineBuilder
@ -49,14 +48,14 @@ namespace PSRule.Rules.Azure.Pipeline
private ResourceGroup _ResourceGroup;
private Subscription _Subscription;
private bool _PassThru;
private string _OutputPath;
internal TemplatePipelineBuilder()
internal TemplatePipelineBuilder(PSRuleOption option)
: base()
{
_DeploymentName = string.Concat(DEPLOYMENTNAME_PREFIX, Guid.NewGuid().ToString().Substring(0, 8));
_ResourceGroup = Data.Template.ResourceGroup.Default;
_Subscription = Data.Template.Subscription.Default;
Configure(option);
}
public void Deployment(string deploymentName)
@ -92,40 +91,77 @@ namespace PSRule.Rules.Azure.Pipeline
_PassThru = passThru;
}
public void OutputPath(string outputPath)
{
_OutputPath = outputPath;
}
private T GetProperty<T>(PSObject obj, string propertyName)
{
return null == obj.Properties[propertyName] ? default(T) : (T)obj.Properties[propertyName].Value;
}
protected override PipelineWriter GetOutput()
{
// Redirect to file instead
if (!string.IsNullOrEmpty(Option.Output.Path))
{
return new FileOutputWriter(
inner: base.GetOutput(),
option: Option,
encoding: GetEncoding(Option.Output.Encoding),
path: Option.Output.Path,
defaultFile: string.Concat(OUTPUTFILE_PREFIX, _DeploymentName, OUTPUTFILE_EXTENSION),
shouldProcess: CmdletContext.ShouldProcess
);
}
return base.GetOutput();
}
protected override PipelineWriter PrepareWriter()
{
var defaultFile = string.Concat(OUTPUTFILE_PREFIX, _DeploymentName, OUTPUTFILE_EXTENSION);
WriteOutput output = (o, enumerate) => WriteToFile(_OutputPath, defaultFile, ShouldProcess, Output, Encoding.UTF8, o);
return _PassThru ? base.PrepareWriter() : new JsonPipelineWriter(output);
return _PassThru ? base.PrepareWriter() : new JsonOutputWriter(GetOutput(), Option);
}
public override IPipeline Build()
{
return new TemplatePipeline(PrepareContext(), PrepareWriter(), _DeploymentName, _ResourceGroup, _Subscription);
}
/// <summary>
/// Get the character encoding for the specified output encoding.
/// </summary>
/// <param name="encoding"></param>
/// <returns></returns>
private static Encoding GetEncoding(OutputEncoding? encoding)
{
switch (encoding)
{
case OutputEncoding.UTF8:
return Encoding.UTF8;
case OutputEncoding.UTF7:
return Encoding.UTF7;
case OutputEncoding.Unicode:
return Encoding.Unicode;
case OutputEncoding.UTF32:
return Encoding.UTF32;
case OutputEncoding.ASCII:
return Encoding.ASCII;
default:
return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
}
}
}
internal sealed class TemplatePipeline : PipelineBase
{
private readonly PipelineWriter _Writer;
private readonly string _DeploymentName;
private readonly ResourceGroup _ResourceGroup;
private readonly Subscription _Subscription;
internal TemplatePipeline(PipelineContext context, PipelineWriter writer, string deploymentName, ResourceGroup resourceGroup, Subscription subscription)
: base(context)
: base(context, writer)
{
_Writer = writer;
_DeploymentName = deploymentName;
_ResourceGroup = resourceGroup;
_Subscription = subscription;
@ -137,15 +173,10 @@ namespace PSRule.Rules.Azure.Pipeline
return;
if (source.ParametersFile == null || source.ParametersFile.Length == 0)
_Writer.Write(ProcessTemplate(source.TemplateFile, null), true);
Writer.WriteObject(ProcessTemplate(source.TemplateFile, null), true);
else
for (var i = 0; i < source.ParametersFile.Length; i++)
_Writer.Write(ProcessTemplate(source.TemplateFile, source.ParametersFile[i]), true);
}
public override void End()
{
_Writer.End();
Writer.WriteObject(ProcessTemplate(source.TemplateFile, source.ParametersFile[i]), true);
}
internal PSObject[] ProcessTemplate(string templateFile, string parametersFile)

108
src/PSRule.Rules.Azure/Resources/Diagnostics.Designer.cs сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PSRule.Rules.Azure.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Diagnostics {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Diagnostics() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Rules.Azure.Resources.Diagnostics", typeof(Diagnostics).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Searching for files in &apos;{0}&apos;..
/// </summary>
internal static string VerboseFindFiles {
get {
return ResourceManager.GetString("VerboseFindFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Found file &apos;{0}&apos;..
/// </summary>
internal static string VerboseFoundFile {
get {
return ResourceManager.GetString("VerboseFoundFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not contain a metadata property..
/// </summary>
internal static string VerboseMetadataNotFound {
get {
return ResourceManager.GetString("VerboseMetadataNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to find the specified template file &apos;{0}&apos;..
/// </summary>
internal static string VerboseTemplateFileNotFound {
get {
return ResourceManager.GetString("VerboseTemplateFileNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not reference a linked template..
/// </summary>
internal static string VerboseTemplateLinkNotFound {
get {
return ResourceManager.GetString("VerboseTemplateLinkNotFound", resourceCulture);
}
}
}
}

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

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="VerboseFindFiles" xml:space="preserve">
<value>Searching for files in '{0}'.</value>
</data>
<data name="VerboseFoundFile" xml:space="preserve">
<value>Found file '{0}'.</value>
</data>
<data name="VerboseMetadataNotFound" xml:space="preserve">
<value>The parameter file '{0}' does not contain a metadata property.</value>
<comment>Occurs when a parameter file does not have the metadata property set.</comment>
</data>
<data name="VerboseTemplateFileNotFound" xml:space="preserve">
<value>Unable to find the specified template file '{0}'.</value>
<comment>Occurs when a template file is specified that doesn't exist.</comment>
</data>
<data name="VerboseTemplateLinkNotFound" xml:space="preserve">
<value>The parameter file '{0}' does not reference a linked template.</value>
<comment>Occurs when a parameter file does not have the metadata.template property set.</comment>
</data>
</root>

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

@ -96,6 +96,15 @@ namespace PSRule.Rules.Azure.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not contain a metadata property..
/// </summary>
internal static string MetadataNotFound {
get {
return ResourceManager.GetString("MetadataNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The number of resource segments needs to match the provided resource type..
/// </summary>
@ -123,6 +132,15 @@ namespace PSRule.Rules.Azure.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The template file &apos;{0}&apos; must be within the current working directory..
/// </summary>
internal static string PathTraversal {
get {
return ResourceManager.GetString("PathTraversal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Read JSON failed..
/// </summary>
@ -168,6 +186,24 @@ namespace PSRule.Rules.Azure.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Unable to find template referenced within parameter file &apos;{0}&apos;..
/// </summary>
internal static string TemplateFileReferenceNotFound {
get {
return ResourceManager.GetString("TemplateFileReferenceNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not reference a linked template..
/// </summary>
internal static string TemplateLinkNotFound {
get {
return ResourceManager.GetString("TemplateLinkNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The template parameter &apos;{0}&apos; does not use the required format..
/// </summary>

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

@ -130,6 +130,10 @@
<data name="FunctionNotFound" xml:space="preserve">
<value>The function "{0}" was not found.</value>
</data>
<data name="MetadataNotFound" xml:space="preserve">
<value>The parameter file '{0}' does not contain a metadata property.</value>
<comment>Occurs when a parameter file does not have the metadata property set.</comment>
</data>
<data name="MismatchingResourceSegments" xml:space="preserve">
<value>The number of resource segments needs to match the provided resource type.</value>
</data>
@ -141,6 +145,10 @@
<value>The parameter named '{0}' was not set or a defaultValue was defined.</value>
<comment>A reference to a parameter that in not in the current context.</comment>
</data>
<data name="PathTraversal" xml:space="preserve">
<value>The template file '{0}' must be within the current working directory.</value>
<comment>Occurs when the template file is outside of the current working path.</comment>
</data>
<data name="ReadJsonFailed" xml:space="preserve">
<value>Read JSON failed.</value>
</data>
@ -157,6 +165,14 @@
<value>Unable to find the specified template file '{0}'.</value>
<comment>Occurs when a template file is specified that doesn't exist.</comment>
</data>
<data name="TemplateFileReferenceNotFound" xml:space="preserve">
<value>Unable to find template referenced within parameter file '{0}'.</value>
<comment>Occurs when a parameter file references a template that doesn't exist.</comment>
</data>
<data name="TemplateLinkNotFound" xml:space="preserve">
<value>The parameter file '{0}' does not reference a linked template.</value>
<comment>Occurs when a parameter file does not have the metadata.template property set.</comment>
</data>
<data name="TemplateParameterInvalid" xml:space="preserve">
<value>The template parameter '{0}' does not use the required format.</value>
<comment>The structure of the template parameter within the parameter file is invalid.</comment>

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

@ -296,3 +296,81 @@ Describe 'Export-AzTemplateRuleData' -Tag 'Cmdlet','Export-AzTemplateRuleData' {
}
#endregion Export-AzTemplateRuleData
#region Get-AzRuleTemplateLink
Describe 'Get-AzRuleTemplateLink' -Tag 'Cmdlet', 'Get-AzRuleTemplateLink' {
# Setup structure for scanning parameter files
$templateScanPath = Join-Path -Path $outputPath -ChildPath 'templates/';
$examplePath = Join-Path -Path $outputPath -ChildPath 'templates/example/';
$Null = New-Item -Path $examplePath -ItemType Directory -Force;
$Null = Copy-Item -Path (Join-Path -Path $here -ChildPath 'Resources.Parameters*.json') -Destination $templateScanPath -Force;
$Null = Copy-Item -Path (Join-Path -Path $here -ChildPath 'Resources.Template*.json') -Destination $templateScanPath -Force;
$Null = Copy-Item -Path (Join-Path -Path $here -ChildPath 'Resources.Parameters*.json') -Destination $examplePath -Force;
$Null = Copy-Item -Path (Join-Path -Path $here -ChildPath 'Resources.Template*.json') -Destination $examplePath -Force;
Context 'With defaults' {
It 'Exports template' {
$getParams = @{
Path = $templateScanPath
InputPath = Join-Path -Path $templateScanPath -ChildPath 'Resources.Parameters*.json'
}
# Get files in specific path
$result = @(Get-AzRuleTemplateLink @getParams);
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 2;
$result.ParameterFile | Should -BeIn @(
(Join-Path -Path $templateScanPath -ChildPath 'Resources.Parameters.json')
(Join-Path -Path $templateScanPath -ChildPath 'Resources.Parameters2.json')
);
# Get Resources.Parameters.json or Resources.Parameters2.json files in shallow path
$result = @(Get-AzRuleTemplateLink -Path $templateScanPath -InputPath './Resources.Parameters?.json');
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 2;
# Get Resources.Parameters.json or Resources.Parameters2.json files in recursive path
$getParams['InputPath'] = 'Resources.Parameters*.json';
$result = @(Get-AzRuleTemplateLink @getParams);
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 4;
# Get Resources.Parameters.json files in recursive path
$result = @(Get-AzRuleTemplateLink -Path $templateScanPath -f '*.Parameters.json');
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 2;
$result.ParameterFile | Should -BeIn @(
(Join-Path -Path $templateScanPath -ChildPath 'Resources.Parameters.json')
(Join-Path -Path $examplePath -ChildPath 'Resources.Parameters.json')
);
}
It 'Handles exceptions' {
$getParams = @{
InputPath = Join-Path -Path $here -ChildPath 'Resources.ParameterFile.Fail.json'
}
# Non-relative path
$Null = Get-AzRuleTemplateLink @getParams -ErrorVariable errorOut -ErrorAction SilentlyContinue;
$errorOut[0].Exception.Message | Should -BeLike "Unable to find template referenced within parameter file '*'.";
# File does not exist
$getParams['InputPath'] = Join-Path -Path $here -ChildPath 'Resources.ParameterFile.Fail2.json';
$Null = Get-AzRuleTemplateLink @getParams -ErrorVariable errorOut -ErrorAction SilentlyContinue;
$errorOut[0].Exception.Message | Should -BeLike "Unable to find template referenced within parameter file '*'.";
# No metadata property
$getParams['InputPath'] = Join-Path -Path $here -ChildPath 'Resources.ParameterFile.Fail3.json';
$Null = Get-AzRuleTemplateLink @getParams -ErrorVariable errorOut -ErrorAction SilentlyContinue;
$errorOut[0].Exception.Message | Should -BeLike "The parameter file '*' does not contain a metadata property.";
# metadata.template property not set
$getParams['InputPath'] = Join-Path -Path $here -ChildPath 'Resources.ParameterFile.Fail4.json';
$Null = Get-AzRuleTemplateLink @getParams -ErrorVariable errorOut -ErrorAction SilentlyContinue;
$errorOut[0].Exception.Message | Should -BeLike "The parameter file '*' does not reference a linked template.";
}
}
}
#endregion Get-AzRuleTemplateLink

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

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Pipeline;
namespace PSRule.Rules.Azure
{
internal sealed class NullLogger : ILogger
{
public void WriteVerbose(string message)
{
// Do nothing
}
public void WriteVerbose(string format, params object[] args)
{
// Do nothing
}
}
}

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

@ -31,9 +31,15 @@
<None Update="Resources.Parameters.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources.Parameters2.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources.Template.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources.Template2.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Pipeline;
using System;
using System.IO;
using Xunit;
namespace PSRule.Rules.Azure
{
public sealed class PathBuilderTests
{
[Fact]
public void Build()
{
var builder = new PathBuilder(new NullLogger(), GetSourcePath(""), "*.json");
builder.Add(GetSourcePath("Resources.Parameters.json"));
var actual1 = builder.Build();
Assert.Single(actual1);
Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual1[0].FullName);
builder.Add(GetSourcePath("Resources.Parameter?.json"));
var actual2 = builder.Build();
Assert.Single(actual2);
Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual2[0].FullName);
builder.Add(GetSourcePath("*Parameters*.json"));
var actual3 = builder.Build();
Assert.Equal(2, actual3.Length);
Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual3[0].FullName);
Assert.Equal(GetSourcePath("Resources.Parameters2.json"), actual3[1].FullName);
builder.Add(GetSourcePath("*Parameters?.json"));
var actual4 = builder.Build();
Assert.Equal(2, actual4.Length);
Assert.Equal(GetSourcePath("Resources.Parameters.json"), actual4[0].FullName);
Assert.Equal(GetSourcePath("Resources.Parameters2.json"), actual4[1].FullName);
}
private static string GetSourcePath(string fileName)
{
return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName));
}
}
}

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

@ -0,0 +1,72 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"template": "Resources.Template.json"
},
"parameters": {
"vnetName": {
"value": "vnet-001"
},
"addressPrefix": {
"value": [
"10.1.0.0/24"
]
},
"subnets": {
"value": [
{
"name": "subnet1",
"addressPrefix": "10.1.0.32/28",
"securityRules": [
{
"name": "deny-rdp-inbound",
"properties": {
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389"
],
"access": "Deny",
"priority": 200,
"direction": "Inbound",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "VirtualNetwork"
}
},
{
"name": "deny-hop-outbound",
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389",
"22"
],
"access": "Deny",
"priority": 200,
"direction": "Outbound",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "*"
}
}
]
},
{
"name": "subnet2",
"addressPrefix": "10.1.0.64/28",
"securityRules": []
}
]
},
"aciSubnet": {
"value": "subnet2"
},
"clusterSubnet": {
"value": "subnet1"
},
"clusterObjectId": {
"value": "00000000-0000-0000-0000-000000000000"
}
}
}

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

@ -0,0 +1,72 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"template": "./Resources.NotATemplate.json"
},
"parameters": {
"vnetName": {
"value": "vnet-001"
},
"addressPrefix": {
"value": [
"10.1.0.0/24"
]
},
"subnets": {
"value": [
{
"name": "subnet1",
"addressPrefix": "10.1.0.32/28",
"securityRules": [
{
"name": "deny-rdp-inbound",
"properties": {
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389"
],
"access": "Deny",
"priority": 200,
"direction": "Inbound",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "VirtualNetwork"
}
},
{
"name": "deny-hop-outbound",
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389",
"22"
],
"access": "Deny",
"priority": 200,
"direction": "Outbound",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "*"
}
}
]
},
{
"name": "subnet2",
"addressPrefix": "10.1.0.64/28",
"securityRules": []
}
]
},
"aciSubnet": {
"value": "subnet2"
},
"clusterSubnet": {
"value": "subnet1"
},
"clusterObjectId": {
"value": "00000000-0000-0000-0000-000000000000"
}
}
}

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

@ -0,0 +1,69 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vnetName": {
"value": "vnet-001"
},
"addressPrefix": {
"value": [
"10.1.0.0/24"
]
},
"subnets": {
"value": [
{
"name": "subnet1",
"addressPrefix": "10.1.0.32/28",
"securityRules": [
{
"name": "deny-rdp-inbound",
"properties": {
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389"
],
"access": "Deny",
"priority": 200,
"direction": "Inbound",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "VirtualNetwork"
}
},
{
"name": "deny-hop-outbound",
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389",
"22"
],
"access": "Deny",
"priority": 200,
"direction": "Outbound",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "*"
}
}
]
},
{
"name": "subnet2",
"addressPrefix": "10.1.0.64/28",
"securityRules": []
}
]
},
"aciSubnet": {
"value": "subnet2"
},
"clusterSubnet": {
"value": "subnet1"
},
"clusterObjectId": {
"value": "00000000-0000-0000-0000-000000000000"
}
}
}

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

@ -0,0 +1,71 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
},
"parameters": {
"vnetName": {
"value": "vnet-001"
},
"addressPrefix": {
"value": [
"10.1.0.0/24"
]
},
"subnets": {
"value": [
{
"name": "subnet1",
"addressPrefix": "10.1.0.32/28",
"securityRules": [
{
"name": "deny-rdp-inbound",
"properties": {
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389"
],
"access": "Deny",
"priority": 200,
"direction": "Inbound",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "VirtualNetwork"
}
},
{
"name": "deny-hop-outbound",
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389",
"22"
],
"access": "Deny",
"priority": 200,
"direction": "Outbound",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "*"
}
}
]
},
{
"name": "subnet2",
"addressPrefix": "10.1.0.64/28",
"securityRules": []
}
]
},
"aciSubnet": {
"value": "subnet2"
},
"clusterSubnet": {
"value": "subnet1"
},
"clusterObjectId": {
"value": "00000000-0000-0000-0000-000000000000"
}
}
}

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

@ -1,6 +1,9 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"template": "./Resources.Template.json"
},
"parameters": {
"vnetName": {
"value": "vnet-001"

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

@ -2,7 +2,7 @@
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"template": "templates/keyvault/v1.0/template.json"
"template": "./Resources.Template2.json"
},
"parameters": {
"vaultName": {

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

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Pipeline;
using System;
using System.IO;
using System.Management.Automation;
using Xunit;
namespace PSRule.Rules.Azure
{
public sealed class TemplateLinkTests
{
[Fact]
public void Pipeline()
{
var pipeline = NewPipeline();
pipeline.Begin();
pipeline.Process(PSObject.AsPSObject(GetSourcePath("Resources.Parameters.json")));
pipeline.Process(PSObject.AsPSObject(new FileInfo(GetSourcePath("Resources.Parameters.json"))));
pipeline.End();
}
private static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}
private static IPipeline NewPipeline()
{
var builder = PipelineBuilder.TemplateLink(AppDomain.CurrentDomain.BaseDirectory);
return builder.Build();
}
}
}