Родитель
33f4ea9c6d
Коммит
7eb69f180c
|
@ -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)
|
||||
|
|
|
@ -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 '{0}'..
|
||||
/// </summary>
|
||||
internal static string VerboseFindFiles {
|
||||
get {
|
||||
return ResourceManager.GetString("VerboseFindFiles", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Found file '{0}'..
|
||||
/// </summary>
|
||||
internal static string VerboseFoundFile {
|
||||
get {
|
||||
return ResourceManager.GetString("VerboseFoundFile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The parameter file '{0}' 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 '{0}'..
|
||||
/// </summary>
|
||||
internal static string VerboseTemplateFileNotFound {
|
||||
get {
|
||||
return ResourceManager.GetString("VerboseTemplateFileNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The parameter file '{0}' 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 '{0}' 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 '{0}' 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 '{0}'..
|
||||
/// </summary>
|
||||
internal static string TemplateFileReferenceNotFound {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateFileReferenceNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The parameter file '{0}' 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 '{0}' 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();
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче