Родитель
c0d08c876d
Коммит
ad9230dffa
|
@ -32,10 +32,10 @@ stages:
|
|||
matrix:
|
||||
Linux:
|
||||
displayName: 'Linux'
|
||||
imageName: 'ubuntu-16.04'
|
||||
imageName: 'ubuntu-latest'
|
||||
MacOS:
|
||||
displayName: 'MacOS'
|
||||
imageName: 'macos-10.13'
|
||||
imageName: 'macOS-latest'
|
||||
Windows:
|
||||
displayName: 'Windows'
|
||||
imageName: 'vs2017-win2016'
|
||||
|
@ -55,6 +55,36 @@ stages:
|
|||
- powershell: Invoke-Build -Configuration $(buildConfiguration) -Build $(Build.BuildNumber)
|
||||
displayName: 'Build module'
|
||||
|
||||
# Run SonarCloud analysis
|
||||
- powershell: dotnet tool install --global dotnet-sonarscanner
|
||||
displayName: 'Install Sonar scanner'
|
||||
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
|
||||
|
||||
- script: dotnet sonarscanner begin /k:"BernieWhite_PSRule_Rules_Azure" /o:"berniewhite-github" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.login=$(sonarQubeToken) /v:"$(Build.BuildNumber)" /d:sonar.cs.vscoveragexml.reportsPaths="reports/" /d:sonar.cs.xunit.reportsPaths="reports/"
|
||||
displayName: 'Prepare SonarCloud'
|
||||
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
|
||||
|
||||
- script: dotnet build
|
||||
displayName: 'Build solution for analysis'
|
||||
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
|
||||
|
||||
- script: dotnet sonarscanner end /d:sonar.login=$(sonarQubeToken)
|
||||
displayName: 'Complete SonarCloud'
|
||||
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
|
||||
|
||||
# DotNet test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: 'Publish unit test results'
|
||||
inputs:
|
||||
testRunTitle: 'DotNet on $(imageName)'
|
||||
testRunner: VSTest
|
||||
testResultsFiles: 'reports/*.trx'
|
||||
mergeTestResults: true
|
||||
platform: $(imageName)
|
||||
configuration: $(buildConfiguration)
|
||||
publishRunAttachments: true
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Pester test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: 'Publish Pester results'
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"default": true,
|
||||
"header-increment": true,
|
||||
"first-header-h1": {
|
||||
"level": 1
|
||||
},
|
||||
"header-style": {
|
||||
"style": "atx"
|
||||
},
|
||||
"ul-style": {
|
||||
"style": "dash"
|
||||
},
|
||||
"list-indent": true,
|
||||
"ul-start-left": true,
|
||||
"ul-indent": {
|
||||
"indent": 2
|
||||
},
|
||||
"no-trailing-spaces": true,
|
||||
"no-hard-tabs": true,
|
||||
"no-reversed-links": true,
|
||||
"no-multiple-blanks": true,
|
||||
"line-length": {
|
||||
"line_length": 100,
|
||||
"code_blocks": false,
|
||||
"tables": false,
|
||||
"headers": true
|
||||
},
|
||||
"commands-show-output": true,
|
||||
"no-missing-space-atx": true,
|
||||
"no-multiple-space-atx": true,
|
||||
"no-missing-space-closed-atx": true,
|
||||
"no-multiple-space-closed-atx": true,
|
||||
"blanks-around-headers": true,
|
||||
"header-start-left": true,
|
||||
"no-duplicate-header": true,
|
||||
"single-h1": true,
|
||||
"no-trailing-punctuation": {
|
||||
"punctuation": ".,;:!"
|
||||
},
|
||||
"no-multiple-space-blockquote": true,
|
||||
"no-blanks-blockquote": true,
|
||||
"ol-prefix": {
|
||||
"style": "one_or_ordered"
|
||||
},
|
||||
"list-marker-space": true,
|
||||
"blanks-around-fences": true,
|
||||
"blanks-around-lists": true,
|
||||
"no-bare-urls": true,
|
||||
"hr-style": {
|
||||
"style": "---"
|
||||
},
|
||||
"no-emphasis-as-header": true,
|
||||
"no-space-in-emphasis": true,
|
||||
"no-space-in-code": true,
|
||||
"no-space-in-links": true,
|
||||
"fenced-code-language": false,
|
||||
"first-line-h1": false,
|
||||
"no-empty-links": true,
|
||||
"proper-names": {
|
||||
"names": [
|
||||
"PowerShell",
|
||||
"JavaScript"
|
||||
],
|
||||
"code_blocks": false
|
||||
},
|
||||
"no-alt-text": true
|
||||
}
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
- Updated `Azure.AKS.Version` to 1.14.8. [#140](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/140)
|
||||
- Updated rules to use type pre-conditions. [#144](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/144)
|
||||
- **Experimental**: Added support for exporting rule data from templates. [#145](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/145)
|
||||
- Added `Export-AzTemplateRuleData` cmdlet to export templates. See cmdlet help for limitations.
|
||||
- Template and parameters are merged, resolving functions, copy loops and conditions.
|
||||
|
||||
## v0.5.0
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29215.179
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Rules.Azure", "src\PSRule.Rules.Azure\PSRule.Rules.Azure.csproj", "{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Rules.Azure.Tests", "tests\PSRule.Rules.Azure.Tests\PSRule.Rules.Azure.Tests.csproj", "{DE4D9717-0307-41F3-897D-1B40ED8235CC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DB365C69-CCDD-47D3-8526-97400A01E2EE}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
16
README.md
16
README.md
|
@ -8,7 +8,8 @@ A suite of rules to validate Azure resources using PSRule.
|
|||
|
||||
This project is to be considered a **proof-of-concept** and **not a supported product**.
|
||||
|
||||
For issues with rules and documentation please check our GitHub [issues](https://github.com/BernieWhite/PSRule.Rules.Azure/issues) page. If you do not see your problem captured, please file a new issue and follow the provided template.
|
||||
For issues with rules and documentation please check our GitHub [issues](https://github.com/BernieWhite/PSRule.Rules.Azure/issues) page.
|
||||
If you do not see your problem captured, please file a new issue and follow the provided template.
|
||||
|
||||
If you have any problems with the [PSRule][engine] engine, please check the project GitHub [issues](https://github.com/Microsoft/PSRule/issues) page instead.
|
||||
|
||||
|
@ -26,7 +27,8 @@ PSRule.Rules.Azure | Validate Azure resources | [latest][module] / [instructions
|
|||
|
||||
### Export resource data
|
||||
|
||||
To validate Azure resources running in a subscription, export the resource data with the `Export-AzRuleData` cmdlet. The `Export-AzRuleData` cmdlet exports a resource graph for one or more subscriptions that can be used for analysis with the rules in this module.
|
||||
To validate Azure resources running in a subscription, export the resource data with the `Export-AzRuleData` cmdlet.
|
||||
The `Export-AzRuleData` cmdlet exports a resource graph for one or more subscriptions that can be used for analysis with the rules in this module.
|
||||
|
||||
By default, resources for the current subscription context are exported. See below for more options.
|
||||
|
||||
|
@ -91,7 +93,8 @@ For example:
|
|||
Export-AzRuleData -All;
|
||||
```
|
||||
|
||||
To filter results to only failed rules, use `Invoke-PSRule -Outcome Fail`. Passed, failed and error results are shown by default.
|
||||
To filter results to only failed rules, use `Invoke-PSRule -Outcome Fail`.
|
||||
Passed, failed and error results are shown by default.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -145,17 +148,20 @@ The following rules are included in the `PSRule.Rules.Azure` module:
|
|||
|
||||
## Language reference
|
||||
|
||||
PSRule.Rules.Azure extends PowerShell the following cmdlets.
|
||||
PSRule.Rules.Azure extends PowerShell with the following cmdlets.
|
||||
|
||||
### Commands
|
||||
|
||||
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 one or more Azure subscriptions.
|
||||
- [Export-AzTemplateRuleData](docs/commands/PSRule.Rules.Azure/en-US/Export-AzTemplateRuleData.md) - Export resource configuration data from Azure templates.
|
||||
|
||||
## Changes and versioning
|
||||
|
||||
Modules in this repository will use the [semantic versioning](http://semver.org/) model to declare breaking changes from v1.0.0. Prior to v1.0.0, breaking changes may be introduced in minor (0.x.0) version increments. For a list of module changes please see the [change log](CHANGELOG.md).
|
||||
Modules in this repository will use the [semantic versioning](http://semver.org/) model to declare breaking changes from v1.0.0.
|
||||
Prior to v1.0.0, breaking changes may be introduced in minor (0.x.0) version increments.
|
||||
For a list of module changes please see the [change log](CHANGELOG.md).
|
||||
|
||||
> Pre-release module versions are created on major commits and can be installed from the PowerShell Gallery. Pre-release versions should be considered experimental. Modules and change log details for pre-releases will be removed as standard releases are made available.
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ Export resource configuration data from one or more Azure subscriptions.
|
|||
### Default (Default)
|
||||
|
||||
```text
|
||||
Export-AzRuleData [[-OutputPath] <String>] [[-Subscription] <String[]>] [[-Tenant] <String[]>]
|
||||
Export-AzRuleData [[-OutputPath] <String>] [-Subscription <String[]>] [-Tenant <String[]>]
|
||||
[-ResourceGroupName <String[]>] [-Tag <Hashtable>] [-PassThru] [-WhatIf] [-Confirm] [<CommonParameters>]
|
||||
```
|
||||
|
||||
|
@ -29,11 +29,12 @@ Export-AzRuleData [[-OutputPath] <String>] [-ResourceGroupName <String[]>] [-Tag
|
|||
|
||||
## DESCRIPTION
|
||||
|
||||
Export resource configuration data from one or more Azure subscriptions.
|
||||
Export resource configuration data from deployed resources in one or more Azure subscriptions.
|
||||
|
||||
If no filters are specified then the current subscription context will be exported. i.e. `Get-AzContext`
|
||||
|
||||
To export all subscriptions contexts use the `-All` switch. When the `-All` switch is used, all subscriptions contexts will be exported. i.e. `Get-AzContext -ListAvailable`
|
||||
To export all subscriptions contexts use the `-All` switch.
|
||||
When the `-All` switch is used, all subscriptions contexts will be exported. i.e. `Get-AzContext -ListAvailable`
|
||||
|
||||
Resource data will be exported to the current working directory by default as JSON files, one per subscription.
|
||||
|
||||
|
@ -46,14 +47,14 @@ PS C:\> Export-AzRuleData
|
|||
```
|
||||
|
||||
```text
|
||||
Directory: C:\
|
||||
Directory: C:\
|
||||
|
||||
Mode LastWriteTime Length Name
|
||||
---- ------------- ------ ----
|
||||
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000001.json
|
||||
```
|
||||
|
||||
Export information from current subscription context.
|
||||
Export resource configuration data from current subscription context.
|
||||
|
||||
### Example 2
|
||||
|
||||
|
@ -62,7 +63,7 @@ PS C:\> Export-AzRuleData -Subscription 'Contoso Production', 'Contoso Non-produ
|
|||
```
|
||||
|
||||
```text
|
||||
Directory: C:\
|
||||
Directory: C:\
|
||||
|
||||
Mode LastWriteTime Length Name
|
||||
---- ------------- ------ ----
|
||||
|
@ -70,7 +71,7 @@ Mode LastWriteTime Length Name
|
|||
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000002.json
|
||||
```
|
||||
|
||||
Export information from subscriptions by name.
|
||||
Export resource configuration data from subscriptions by name.
|
||||
|
||||
### Example 3
|
||||
|
||||
|
@ -79,17 +80,34 @@ PS C:\> Export-AzRuleData -ResourceGroupName 'rg-app1-web', 'rg-app1-db'
|
|||
```
|
||||
|
||||
```text
|
||||
Directory: C:\
|
||||
Directory: C:\
|
||||
|
||||
Mode LastWriteTime Length Name
|
||||
---- ------------- ------ ----
|
||||
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000001.json
|
||||
```
|
||||
|
||||
Export information from two resource groups within the current subscription context.
|
||||
Export resource configuration data from two resource groups within the current subscription context.
|
||||
|
||||
## PARAMETERS
|
||||
|
||||
### -All
|
||||
|
||||
By default, resources from the current subscription context are extracted.
|
||||
Use `-All` to extract resource data for all subscription contexts instead.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: All
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -OutputPath
|
||||
|
||||
The path to store generated JSON files containing resources.
|
||||
|
@ -106,33 +124,18 @@ Accept pipeline input: False
|
|||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Subscription
|
||||
### -PassThru
|
||||
|
||||
Optionally filter resources by subscription, Id or Name.
|
||||
By default, FileInfo objects are returned to the pipeline for each JSON file created.
|
||||
When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
|
||||
|
||||
```yaml
|
||||
Type: String[]
|
||||
Parameter Sets: Default
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: 1
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Tenant
|
||||
|
||||
Optionally filter resources by a unique Tenant identifer.
|
||||
|
||||
```yaml
|
||||
Type: String[]
|
||||
Parameter Sets: Default
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: 2
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
|
@ -154,6 +157,22 @@ Accept pipeline input: False
|
|||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Subscription
|
||||
|
||||
Optionally filter resources by subscription, Id or Name.
|
||||
|
||||
```yaml
|
||||
Type: String[]
|
||||
Parameter Sets: Default
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Tag
|
||||
|
||||
Optionally filter resources based on tag.
|
||||
|
@ -170,29 +189,13 @@ Accept pipeline input: False
|
|||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -PassThru
|
||||
### -Tenant
|
||||
|
||||
By default, FileInfo objects are returned to the pipeline for each JSON file created. When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
|
||||
Optionally filter resources by a unique Tenant identifer.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -All
|
||||
|
||||
By default, resources from the current subscription context are extracted. Use `-All` to extract resource data for all subscription contexts instead.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: All
|
||||
Type: String[]
|
||||
Parameter Sets: Default
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
|
@ -246,8 +249,14 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
|
|||
|
||||
### System.IO.FileInfo
|
||||
|
||||
Return `FileInfo` for each of the output files created, one per subscription.
|
||||
This is the default.
|
||||
|
||||
### PSObject
|
||||
|
||||
Return an object for each Azure resource, and configuration exported.
|
||||
This is returned when the `-PassThru` switch is used.
|
||||
|
||||
## NOTES
|
||||
|
||||
## RELATED LINKS
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
external help file: PSRule.Rules.Azure-help.xml
|
||||
Module Name: PSRule.Rules.Azure
|
||||
online version: https://github.com/BernieWhite/PSRule.Rules.Azure/blob/master/docs/commands/PSRule.Rules.Azure/en-US/Export-AzTemplateRuleData.md
|
||||
schema: 2.0.0
|
||||
---
|
||||
|
||||
# Export-AzTemplateRuleData
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
Export resource configuration data from Azure templates.
|
||||
|
||||
## SYNTAX
|
||||
|
||||
```text
|
||||
Export-AzTemplateRuleData [-TemplateFile] <String> [[-ParameterFile] <String[]>]
|
||||
[[-ResourceGroupName] <String>] [[-Subscription] <String>] [[-OutputPath] <String>] [-PassThru]
|
||||
[<CommonParameters>]
|
||||
```
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
Export resource configuration data by merging Azure Resource Manager (ARM) template and parameter files.
|
||||
Template and parameters are merged by resolving template parameters, variables and functions.
|
||||
|
||||
By default this is an offline process, requiring no connectivity to Azure.
|
||||
Some functions that may be included in templates dynamically query Azure for current state.
|
||||
For these functions standard placeholder values are used by default.
|
||||
|
||||
Functions that use placeholders include `subscription`, `resourceGroup`, `reference`, `list*`.
|
||||
|
||||
This function does not check template files for strict compliance with Azure schemas.
|
||||
|
||||
Currently the following limitations also apply:
|
||||
|
||||
- Deployments are not expanded. Deployment is returned instead of resources in deployment.
|
||||
- The following functions are not supported:
|
||||
- Array: `array`, `coalesce`, `createArray`, `intersection`
|
||||
- Deployment: `deployment`
|
||||
- Resource: `providers`
|
||||
- String: `dataUri`, `dataUriToString`
|
||||
- References to Key Vault secrets are not expanded. A placeholder value is used instead.
|
||||
- Multi-line strings are not supported.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
### Example 1
|
||||
|
||||
```powershell
|
||||
PS C:\> Export-AzTemplateRuleData -TemplateFile .\azuredeploy.json -ParameterFile .\azuredeploy.parameters.json -OutputPath .\out-deploy.json
|
||||
```
|
||||
|
||||
Export resource configuration data based on merging a template and parameter file together.
|
||||
|
||||
## PARAMETERS
|
||||
|
||||
### -TemplateFile
|
||||
|
||||
The absolute or relative file path to an Azure Resource Manager template file.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: 0
|
||||
Default value: None
|
||||
Accept pipeline input: True (ByPropertyName)
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ParameterFile
|
||||
|
||||
The absolute or relative file path to one or more Azure Resource Manager template parameter files.
|
||||
|
||||
```yaml
|
||||
Type: String[]
|
||||
Parameter Sets: (All)
|
||||
Aliases: TemplateParameterFile
|
||||
|
||||
Required: False
|
||||
Position: 1
|
||||
Default value: None
|
||||
Accept pipeline input: True (ByPropertyName)
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -OutputPath
|
||||
|
||||
The path to store generated JSON files containing resources.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: 4
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -PassThru
|
||||
|
||||
By default, FileInfo objects are returned to the pipeline for each JSON file created.
|
||||
When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ResourceGroupName
|
||||
|
||||
The name of the Resource Group where the deployment will occur.
|
||||
If this option is specified, the properties of the Resource Group will be looked up and used during export.
|
||||
|
||||
This Resource Group specified here will be used to resolve the `resourceGroup()` function.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: 2
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Subscription
|
||||
|
||||
The name of the Subscription where the deployment will occur.
|
||||
If this option is specified, the properties of the Subscription will be looked up and used during export.
|
||||
|
||||
This subscription specified here will be used to resolve the `subscription()` function.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: 3
|
||||
Default value: None
|
||||
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
|
||||
|
||||
### System.String[]
|
||||
|
||||
## OUTPUTS
|
||||
|
||||
### System.Object
|
||||
|
||||
## NOTES
|
||||
|
||||
## RELATED LINKS
|
|
@ -151,8 +151,8 @@ task PSScriptAnalyzer NuGet, {
|
|||
|
||||
# Synopsis: Install PSRule
|
||||
task PSRule NuGet, {
|
||||
if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.10.0 -ErrorAction Ignore)) {
|
||||
Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.10.0 -Scope CurrentUser -Force;
|
||||
if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.11.0 -ErrorAction Ignore)) {
|
||||
Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.11.0 -Scope CurrentUser -Force;
|
||||
}
|
||||
Import-Module -Name PSRule -Verbose:$False;
|
||||
}
|
||||
|
@ -186,14 +186,37 @@ task ModuleDependencies NuGet, PSRule, {
|
|||
}
|
||||
}
|
||||
|
||||
task BuildDotNet {
|
||||
exec {
|
||||
# Build library
|
||||
# Add build version -p:versionPrefix=$ModuleVersion
|
||||
dotnet publish src/PSRule.Rules.Azure -c $Configuration -f netstandard2.0 -o $(Join-Path -Path $PWD -ChildPath out/modules/PSRule.Rules.Azure)
|
||||
}
|
||||
}
|
||||
|
||||
task TestDotNet {
|
||||
if ($CodeCoverage) {
|
||||
exec {
|
||||
# Test library
|
||||
dotnet test --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.Azure.Tests
|
||||
}
|
||||
}
|
||||
else {
|
||||
exec {
|
||||
# Test library
|
||||
dotnet test --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.Azure.Tests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task CopyModule {
|
||||
CopyModuleFiles -Path src/PSRule.Rules.Azure -DestinationPath out/modules/PSRule.Rules.Azure;
|
||||
}
|
||||
|
||||
# Synopsis: Build modules only
|
||||
task BuildModule CopyModule
|
||||
task BuildModule BuildDotNet, CopyModule
|
||||
|
||||
task TestRules PSRule, Pester, PSScriptAnalyzer, {
|
||||
task TestRules TestDotNet, PSRule, Pester, PSScriptAnalyzer, {
|
||||
# Run Pester tests
|
||||
$pesterParams = @{ Path = $PWD; OutputFile = 'reports/pester-unit.xml'; OutputFormat = 'NUnitXml'; PesterOption = @{ IncludeVSCodeMarker = $True }; PassThru = $True; };
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ repository:
|
|||
bugs:
|
||||
url: https://github.com/BernieWhite/PSRule.Rules.Azure/issues
|
||||
|
||||
modules:
|
||||
PSRule: ^0.11.0
|
||||
|
||||
tasks:
|
||||
clear:
|
||||
steps:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[*.cs]
|
||||
|
||||
# Suppressed, issue with Azure DevOps build on MacOS, not supported
|
||||
# IDE0034: Simplify 'default' expression
|
||||
csharp_prefer_simple_default_expression = true:silent
|
|
@ -0,0 +1,221 @@
|
|||
using Newtonsoft.Json;
|
||||
using PSRule.Rules.Azure.Pipeline;
|
||||
using PSRule.Rules.Azure.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
|
||||
namespace PSRule.Rules.Azure
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML.
|
||||
/// </summary>
|
||||
internal sealed class PSObjectJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(PSObject);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (!(value is PSObject obj))
|
||||
throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value));
|
||||
|
||||
if (value is FileSystemInfo fileSystemInfo)
|
||||
{
|
||||
WriteFileSystemInfo(writer, fileSystemInfo, serializer);
|
||||
return;
|
||||
}
|
||||
writer.WriteStartObject();
|
||||
foreach (var property in obj.Properties)
|
||||
{
|
||||
// Ignore properties that are not readable or can cause race condition
|
||||
if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo)
|
||||
continue;
|
||||
|
||||
writer.WritePropertyName(property.Name);
|
||||
serializer.Serialize(writer, property.Value);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
// Create target object based on JObject
|
||||
var result = existingValue as PSObject ?? new PSObject();
|
||||
|
||||
// Read tokens
|
||||
ReadObject(value: result, reader: reader);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ReadObject(PSObject value, JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
|
||||
|
||||
reader.Read();
|
||||
string name = null;
|
||||
|
||||
// Read each token
|
||||
while (reader.TokenType != JsonToken.EndObject)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.PropertyName:
|
||||
name = reader.Value.ToString();
|
||||
break;
|
||||
|
||||
case JsonToken.StartObject:
|
||||
var child = new PSObject();
|
||||
ReadObject(value: child, reader: reader);
|
||||
value.Properties.Add(new PSNoteProperty(name: name, value: child));
|
||||
break;
|
||||
|
||||
case JsonToken.StartArray:
|
||||
var items = new List<object>();
|
||||
reader.Read();
|
||||
|
||||
while (reader.TokenType != JsonToken.EndArray)
|
||||
{
|
||||
items.Add(ReadValue(reader));
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray()));
|
||||
break;
|
||||
|
||||
default:
|
||||
value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value));
|
||||
break;
|
||||
}
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
private object ReadValue(JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
return reader.Value;
|
||||
|
||||
var value = new PSObject();
|
||||
ReadObject(value, reader);
|
||||
return value;
|
||||
}
|
||||
|
||||
private void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer)
|
||||
{
|
||||
serializer.Serialize(writer, value.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom serializer to convert PSObjects that may or maynot be in a JSON array to an a PSObject array.
|
||||
/// </summary>
|
||||
internal sealed class PSObjectArrayJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(PSObject[]);
|
||||
}
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.StartArray)
|
||||
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
|
||||
|
||||
var result = new List<PSObject>();
|
||||
var isArray = reader.TokenType == JsonToken.StartArray;
|
||||
|
||||
if (isArray)
|
||||
reader.Read();
|
||||
|
||||
while (!isArray || (isArray && reader.TokenType != JsonToken.EndArray))
|
||||
{
|
||||
var value = ReadObject(reader: reader);
|
||||
result.Add(value);
|
||||
|
||||
// Consume the EndObject token
|
||||
if (isArray)
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private PSObject ReadObject(JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
|
||||
|
||||
reader.Read();
|
||||
var result = new PSObject();
|
||||
string name = null;
|
||||
|
||||
// Read each token
|
||||
while (reader.TokenType != JsonToken.EndObject)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.PropertyName:
|
||||
name = reader.Value.ToString();
|
||||
break;
|
||||
|
||||
case JsonToken.StartObject:
|
||||
var value = ReadObject(reader: reader);
|
||||
result.Properties.Add(new PSNoteProperty(name: name, value: value));
|
||||
break;
|
||||
|
||||
case JsonToken.StartArray:
|
||||
var items = ReadArray(reader: reader);
|
||||
result.Properties.Add(new PSNoteProperty(name: name, value: items));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
result.Properties.Add(new PSNoteProperty(name: name, value: reader.Value));
|
||||
break;
|
||||
}
|
||||
reader.Read();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private PSObject[] ReadArray(JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartArray)
|
||||
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
|
||||
|
||||
reader.Read();
|
||||
var result = new List<PSObject>();
|
||||
|
||||
while (reader.TokenType != JsonToken.EndArray)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
result.Add(ReadObject(reader: reader));
|
||||
}
|
||||
else if (reader.TokenType == JsonToken.StartArray)
|
||||
{
|
||||
result.Add(PSObject.AsPSObject(ReadArray(reader)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(PSObject.AsPSObject(reader.Value));
|
||||
}
|
||||
reader.Read();
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System.Management.Automation;
|
||||
|
||||
namespace PSRule.Rules.Azure
|
||||
{
|
||||
internal static class PSObjectExtensions
|
||||
{
|
||||
internal static T GetPropertyValue<T>(this PSObject obj, string propertyName)
|
||||
{
|
||||
return (T)obj.Properties[propertyName].Value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
|
||||
namespace PSRule.Rules.Azure.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A delgate to allow callback to PowerShell to get current working path.
|
||||
/// </summary>
|
||||
internal delegate string PathDelegate();
|
||||
|
||||
public sealed class PSRuleOption
|
||||
{
|
||||
/// <summary>
|
||||
/// A callback that is overridden by PowerShell so that the current working path can be retrieved.
|
||||
/// </summary>
|
||||
private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// Set working path from PowerShell host environment.
|
||||
/// </summary>
|
||||
/// <param name="executionContext">An $ExecutionContext object.</param>
|
||||
/// <remarks>
|
||||
/// Called from PowerShell.
|
||||
/// </remarks>
|
||||
public static void UseExecutionContext(EngineIntrinsics executionContext)
|
||||
{
|
||||
if (executionContext == null)
|
||||
{
|
||||
_GetWorkingPath = () => Directory.GetCurrentDirectory();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path;
|
||||
}
|
||||
|
||||
public static string GetWorkingPath()
|
||||
{
|
||||
return _GetWorkingPath();
|
||||
}
|
||||
|
||||
/// <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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using PSRule.Rules.Azure.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
|
||||
|
||||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
internal delegate object ExpressionFnOuter(TemplateContext context);
|
||||
internal delegate object ExpressionFn(TemplateContext context, object[] args);
|
||||
|
||||
internal sealed class ExpressionBuilder
|
||||
{
|
||||
private readonly ExpressionFactory _Functions;
|
||||
|
||||
internal ExpressionBuilder()
|
||||
{
|
||||
_Functions = new ExpressionFactory();
|
||||
}
|
||||
|
||||
internal ExpressionFnOuter Build(string s)
|
||||
{
|
||||
return Lexer(Parse(s));
|
||||
}
|
||||
|
||||
private TokenStream Parse(string s)
|
||||
{
|
||||
return ExpressionParser.Parse(s);
|
||||
}
|
||||
|
||||
private ExpressionFnOuter Lexer(TokenStream stream)
|
||||
{
|
||||
return stream.TryTokenType(ExpressionTokenType.Element, out ExpressionToken element) ? Element(stream, element) : null;
|
||||
}
|
||||
|
||||
private ExpressionFnOuter Element(TokenStream stream, ExpressionToken element)
|
||||
{
|
||||
ExpressionFnOuter result = null;
|
||||
|
||||
// function
|
||||
if (stream.Skip(ExpressionTokenType.GroupStart))
|
||||
{
|
||||
if (!_Functions.TryDescriptor(element.Content, out IFunctionDescriptor descriptor))
|
||||
throw new NotImplementedException(string.Format(PSRuleResources.FunctionNotFound, element.Content));
|
||||
|
||||
var fnParams = new List<ExpressionFnOuter>();
|
||||
while (!stream.Skip(ExpressionTokenType.GroupEnd))
|
||||
{
|
||||
fnParams.Add(Inner(stream));
|
||||
}
|
||||
var aParams = fnParams.ToArray();
|
||||
|
||||
result = (context) => descriptor.Invoke(context, aParams);
|
||||
|
||||
while (stream.TryTokenType(ExpressionTokenType.IndexStart, out ExpressionToken token) || stream.TryTokenType(ExpressionTokenType.Property, out token))
|
||||
{
|
||||
if (token.Type == ExpressionTokenType.IndexStart)
|
||||
{
|
||||
// Invert: index(fn1(p1, p2), 0)
|
||||
var inner = Inner(stream);
|
||||
ExpressionFnOuter outer = AddIndex(result, inner);
|
||||
result = outer;
|
||||
|
||||
stream.Skip(ExpressionTokenType.IndexEnd);
|
||||
}
|
||||
else if (token.Type == ExpressionTokenType.Property)
|
||||
{
|
||||
// Invert: property(fn1(p1, p2), "name")
|
||||
ExpressionFnOuter outer = AddProperty(result, token.Content);
|
||||
result = outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
// integer
|
||||
else
|
||||
{
|
||||
result = (context) => element.Content;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ExpressionFnOuter Inner(TokenStream stream)
|
||||
{
|
||||
ExpressionFnOuter result = null;
|
||||
if (stream.TryTokenType(ExpressionTokenType.String, out ExpressionToken token))
|
||||
{
|
||||
result = (context) => token.Content;
|
||||
}
|
||||
else if (stream.TryTokenType(ExpressionTokenType.Element, out token))
|
||||
{
|
||||
result = Element(stream, token);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ExpressionFnOuter AddIndex(ExpressionFnOuter inner, ExpressionFnOuter innerInner)
|
||||
{
|
||||
return (context) => Index(context, inner, innerInner);
|
||||
}
|
||||
|
||||
private static object Index(TemplateContext context, ExpressionFnOuter inner, ExpressionFnOuter index)
|
||||
{
|
||||
var array = inner(context);
|
||||
var indexResult = index(context);
|
||||
if (indexResult is string ixs)
|
||||
return Index(array, int.Parse(ixs));
|
||||
else
|
||||
return Index(array, (int)indexResult);
|
||||
}
|
||||
|
||||
private static object Index(object value, int index)
|
||||
{
|
||||
if (value is JArray jArray)
|
||||
return jArray[index];
|
||||
|
||||
else if (value is Array array)
|
||||
return array.GetValue(index);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ExpressionFnOuter AddProperty(ExpressionFnOuter inner, string propertyName)
|
||||
{
|
||||
return (context) => Property(context, inner, propertyName);
|
||||
}
|
||||
|
||||
private static object Property(TemplateContext context, ExpressionFnOuter inner, string propertyName)
|
||||
{
|
||||
var result = inner(context);
|
||||
if (result is JToken jt)
|
||||
return jt[propertyName].Value<object>();
|
||||
|
||||
if (result is JObject jobj)
|
||||
return jobj[propertyName].Value<object>();
|
||||
|
||||
if (result is MockResource mockResource)
|
||||
return JToken.FromObject(string.Concat("{{Resource.", propertyName, "}}"));
|
||||
|
||||
if (result is MockResourceList mockResourceList)
|
||||
return JToken.FromObject(string.Concat("{{List.", propertyName, "}}"));
|
||||
|
||||
return PropertyOrField(result, propertyName);
|
||||
}
|
||||
|
||||
private static object PropertyOrField(object obj, string propertyName)
|
||||
{
|
||||
// Try property
|
||||
var resultType = obj.GetType();
|
||||
var property = resultType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public);
|
||||
if (property != null)
|
||||
return property.GetValue(obj);
|
||||
|
||||
// Try field
|
||||
var field = resultType.GetField(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public);
|
||||
if (field == null)
|
||||
throw new ArgumentException();
|
||||
|
||||
return field.GetValue(obj);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ExpressionFactory
|
||||
{
|
||||
private readonly Dictionary<string, IFunctionDescriptor> _Descriptors;
|
||||
|
||||
public ExpressionFactory()
|
||||
{
|
||||
_Descriptors = new Dictionary<string, IFunctionDescriptor>();
|
||||
foreach (var d in Functions.Builtin)
|
||||
With(d);
|
||||
}
|
||||
|
||||
public bool TryDescriptor(string name, out IFunctionDescriptor descriptor)
|
||||
{
|
||||
return IsList(name) ? _Descriptors.TryGetValue("list", out descriptor) : _Descriptors.TryGetValue(name, out descriptor);
|
||||
}
|
||||
|
||||
private void With(IFunctionDescriptor descriptor)
|
||||
{
|
||||
_Descriptors.Add(descriptor.Name, descriptor);
|
||||
}
|
||||
|
||||
private static bool IsList(string name)
|
||||
{
|
||||
return name.StartsWith("list", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("Function: {Name}")]
|
||||
internal sealed class FunctionDescriptor : IFunctionDescriptor
|
||||
{
|
||||
private readonly ExpressionFn Fn;
|
||||
|
||||
public FunctionDescriptor(string name, ExpressionFn fn)
|
||||
{
|
||||
Name = name;
|
||||
Fn = fn;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public object Invoke(TemplateContext context, ExpressionFnOuter[] args)
|
||||
{
|
||||
var parameters = new object[args.Length];
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
parameters[i] = args[i](context);
|
||||
|
||||
return Fn(context, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IFunctionDescriptor
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
object Invoke(TemplateContext context, ExpressionFnOuter[] args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class used to parse template expressions.
|
||||
/// </summary>
|
||||
internal static class ExpressionParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Tokenize an expression.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression.</param>
|
||||
/// <returns>A stream of tokens representing the expression.</returns>
|
||||
/// <example>
|
||||
/// [parameters('vnetName')]
|
||||
/// [concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]
|
||||
/// [concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]
|
||||
/// </example>
|
||||
internal static TokenStream Parse(string expression)
|
||||
{
|
||||
var stream = new ExpressionStream(expression);
|
||||
var output = new TokenStream();
|
||||
stream.Start();
|
||||
while (!stream.End())
|
||||
{
|
||||
if (stream.TryElement(out string element))
|
||||
{
|
||||
output.Function(element);
|
||||
Function(stream, output, element);
|
||||
}
|
||||
if (Index(stream, output))
|
||||
{
|
||||
stream.Separator();
|
||||
}
|
||||
if (stream.CaptureProperty(out string propertyName))
|
||||
{
|
||||
output.Property(propertyName);
|
||||
stream.Separator();
|
||||
}
|
||||
}
|
||||
output.MoveTo(0);
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enter a function.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// function()
|
||||
/// </example>
|
||||
private static bool Function(ExpressionStream stream, TokenStream output, string element)
|
||||
{
|
||||
// Look for '('
|
||||
if (!stream.IsGroupStart())
|
||||
return false;
|
||||
|
||||
output.GroupStart();
|
||||
while (!stream.IsGroupEnd())
|
||||
Inner(stream, output);
|
||||
|
||||
output.GroupEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enter an index.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// function()[0]
|
||||
/// </example>
|
||||
private static bool Index(ExpressionStream stream, TokenStream output)
|
||||
{
|
||||
// Look for '['
|
||||
if (!stream.IsIndexStart())
|
||||
return false;
|
||||
|
||||
output.IndexStart();
|
||||
while (!stream.IsIndexEnd())
|
||||
Inner(stream, output);
|
||||
|
||||
output.IndexEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse inner tokens.
|
||||
/// </summary>
|
||||
private static void Inner(ExpressionStream stream, TokenStream output)
|
||||
{
|
||||
if (stream.CaptureString(out string s))
|
||||
{
|
||||
output.String(s);
|
||||
stream.Separator();
|
||||
}
|
||||
else if (Index(stream, output))
|
||||
{
|
||||
stream.Separator();
|
||||
}
|
||||
else if (stream.CaptureProperty(out string propertyName))
|
||||
{
|
||||
output.Property(propertyName);
|
||||
stream.Separator();
|
||||
}
|
||||
else if (stream.TryElement(out string element))
|
||||
{
|
||||
output.Function(element);
|
||||
Function(stream, output, element);
|
||||
stream.Separator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// A string stream for reading tokenizing template expressions.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Position = {Position}, Current = {Current}")]
|
||||
internal sealed class ExpressionStream
|
||||
{
|
||||
private readonly string _Source;
|
||||
private readonly int _Length;
|
||||
|
||||
/// <summary>
|
||||
/// The current character position in the expression string. Call Next() to change the position.
|
||||
/// </summary>
|
||||
private int _Position;
|
||||
private int _Line;
|
||||
private int _Column;
|
||||
private char _Current;
|
||||
private char _Previous;
|
||||
private int _EscapeLength;
|
||||
|
||||
// The maximum length of a template expression
|
||||
private const int MaxLength = 24576;
|
||||
|
||||
private const char Whitespace = ' ';
|
||||
|
||||
|
||||
private const char Backslash = '\\';
|
||||
|
||||
private const char Apostrophe = '\'';
|
||||
private const char Comma = ',';
|
||||
private const char Period = '.';
|
||||
private const char ParenthesesOpen = '(';
|
||||
private const char ParenthesesClose = ')';
|
||||
private const char BracketOpen = '[';
|
||||
private const char BracketClose = ']';
|
||||
private readonly static char[] FunctionNameStopCharacter = new char[] { '(', ']', '[', ')', '\'', ' ', ',' };
|
||||
private readonly static char[] StringStopCharacters = new char[] { '\'' };
|
||||
private readonly static char[] PropertyStopCharacters = new char[] { '(', ']', '[', ',', ')', ' ', '\'' };
|
||||
|
||||
internal ExpressionStream(string expression)
|
||||
{
|
||||
_Source = expression;
|
||||
_Length = _Source.Length;
|
||||
_Position = 0;
|
||||
_Line = 0;
|
||||
_Column = 0;
|
||||
_EscapeLength = 0;
|
||||
|
||||
if (_Length < 0 || _Length > MaxLength)
|
||||
throw new ArgumentOutOfRangeException(nameof(expression));
|
||||
|
||||
UpdateCurrent();
|
||||
|
||||
if (_Source.Length > 0)
|
||||
_Line = 1;
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool EOF
|
||||
{
|
||||
get { return _Position >= _Length; }
|
||||
}
|
||||
|
||||
public bool IsStartOfLine
|
||||
{
|
||||
get { return _Column == 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The character at the current position in the stream.
|
||||
/// </summary>
|
||||
public char Current
|
||||
{
|
||||
get { return _Current; }
|
||||
}
|
||||
|
||||
public char Previous
|
||||
{
|
||||
get { return _Previous; }
|
||||
}
|
||||
|
||||
public int Line
|
||||
{
|
||||
get { return _Line; }
|
||||
}
|
||||
|
||||
public int Column
|
||||
{
|
||||
get { return _Column; }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
/// <summary>
|
||||
/// Used for interactive debugging of current position and next characters in the stream.
|
||||
/// </summary>
|
||||
public string Preview
|
||||
{
|
||||
get { return _Source.Substring(_Position); }
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public int Position
|
||||
{
|
||||
get { return _Position; }
|
||||
}
|
||||
|
||||
private int Remaining
|
||||
{
|
||||
get { return _Length - Position; }
|
||||
}
|
||||
|
||||
public bool IsEscaped
|
||||
{
|
||||
get { return _EscapeLength > 0; }
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
return Skip(BracketOpen);
|
||||
}
|
||||
|
||||
public bool End()
|
||||
{
|
||||
return Skip(BracketClose);
|
||||
}
|
||||
|
||||
public bool TryElement(out string element)
|
||||
{
|
||||
SkipWhitespace();
|
||||
element = CaptureUntil(FunctionNameStopCharacter);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsGroupStart()
|
||||
{
|
||||
return Skip(ParenthesesOpen);
|
||||
}
|
||||
|
||||
public bool IsGroupEnd()
|
||||
{
|
||||
return Skip(ParenthesesClose);
|
||||
}
|
||||
|
||||
public bool IsString()
|
||||
{
|
||||
return Skip(Apostrophe);
|
||||
}
|
||||
|
||||
public bool CaptureString(out string s)
|
||||
{
|
||||
s = null;
|
||||
if (!IsString())
|
||||
return false;
|
||||
|
||||
s = CaptureUntil(StringStopCharacters);
|
||||
IsString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Separator()
|
||||
{
|
||||
SkipWhitespace();
|
||||
Skip(Comma);
|
||||
SkipWhitespace();
|
||||
}
|
||||
|
||||
public bool IsIndexStart()
|
||||
{
|
||||
return Skip(BracketOpen);
|
||||
}
|
||||
|
||||
public bool IsIndexEnd()
|
||||
{
|
||||
return Skip(BracketClose);
|
||||
}
|
||||
|
||||
public bool IsProperty()
|
||||
{
|
||||
return Skip(Period);
|
||||
}
|
||||
|
||||
public bool CaptureProperty(out string propertyName)
|
||||
{
|
||||
propertyName = null;
|
||||
if (!IsProperty())
|
||||
return false;
|
||||
|
||||
propertyName = CaptureUntil(PropertyStopCharacters);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skip if the current character is whitespace.
|
||||
/// </summary>
|
||||
public void SkipWhitespace()
|
||||
{
|
||||
Skip(Whitespace, max: 0);
|
||||
}
|
||||
|
||||
public bool Skip(char c)
|
||||
{
|
||||
if (_Current != c)
|
||||
return false;
|
||||
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skip ahead if the current character is expected. Keep skipping when the character is repeated.
|
||||
/// </summary>
|
||||
/// <param name="c">The character to skip.</param>
|
||||
/// <returns>The number of characters that where skipped.</returns>
|
||||
public int Skip(char c, int max)
|
||||
{
|
||||
var skipped = 0;
|
||||
|
||||
while (Current == c && (max == 0 || skipped < max))
|
||||
{
|
||||
Next();
|
||||
|
||||
skipped++;
|
||||
}
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move to the next character in the stream.
|
||||
/// </summary>
|
||||
/// <returns>Is True when more characters exist in the stream.</returns>
|
||||
public bool Next(bool ignoreEscaping = false)
|
||||
{
|
||||
_Position += _EscapeLength > 0 ? _EscapeLength + 1 : 1;
|
||||
|
||||
if (_Position >= _Length)
|
||||
{
|
||||
_Current = char.MinValue;
|
||||
return false;
|
||||
}
|
||||
UpdateCurrent(ignoreEscaping);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateCurrent(bool ignoreEscaping = false)
|
||||
{
|
||||
// Handle escape sequences
|
||||
_EscapeLength = ignoreEscaping ? 0 : GetEscapeCount(_Position);
|
||||
|
||||
_Previous = _Current;
|
||||
_Current = _Source[_Position + _EscapeLength];
|
||||
}
|
||||
|
||||
private int GetEscapeCount(int position)
|
||||
{
|
||||
// Check for escape sequences
|
||||
if (position < _Length && _Source[position] == Backslash)
|
||||
{
|
||||
var next = _Source[position + 1];
|
||||
|
||||
// Check against list of escapable characters
|
||||
if (next == Backslash || next == BracketOpen || next == ParenthesesOpen || next == BracketClose || next == ParenthesesClose)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public string CaptureUntil(char[] c, bool ignoreEscaping = false)
|
||||
{
|
||||
var start = Position;
|
||||
var length = 0;
|
||||
|
||||
while (!EOF)
|
||||
{
|
||||
if (!IsEscaped && c.Contains(Current))
|
||||
break;
|
||||
|
||||
length++;
|
||||
Next(ignoreEscaping);
|
||||
}
|
||||
return Substring(start, length, ignoreEscaping);
|
||||
}
|
||||
|
||||
private string Substring(int start, int length, bool ignoreEscaping = false)
|
||||
{
|
||||
if (ignoreEscaping)
|
||||
return _Source.Substring(start, length);
|
||||
|
||||
var position = start;
|
||||
var i = 0;
|
||||
var buffer = new char[length];
|
||||
while (i < length)
|
||||
{
|
||||
var offset = GetEscapeCount(position);
|
||||
buffer[i] = _Source[position + offset];
|
||||
position += offset + 1;
|
||||
i++;
|
||||
}
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,593 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using static PSRule.Rules.Azure.Data.Template.DeploymentTemplate;
|
||||
|
||||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
public delegate T StringExpression<T>();
|
||||
|
||||
/// <summary>
|
||||
/// The base class for a template visitor.
|
||||
/// </summary>
|
||||
internal abstract class TemplateVisitor
|
||||
{
|
||||
private readonly Subscription _Subscription;
|
||||
private readonly ResourceGroup _ResourceGroup;
|
||||
|
||||
private TemplateContext _Context;
|
||||
|
||||
internal TemplateVisitor(Subscription subscription, ResourceGroup resourceGroup)
|
||||
{
|
||||
_Subscription = subscription;
|
||||
_ResourceGroup = resourceGroup;
|
||||
}
|
||||
|
||||
public sealed class TemplateContext
|
||||
{
|
||||
internal TemplateContext()
|
||||
{
|
||||
Resources = new List<JObject>();
|
||||
Parameters = new Dictionary<string, object>();
|
||||
Variables = new Dictionary<string, object>();
|
||||
CopyIndex = new CopyIndexStore();
|
||||
ResourceGroup = new ResourceGroup();
|
||||
Subscription = new Subscription();
|
||||
}
|
||||
|
||||
internal TemplateContext(Subscription subscription, ResourceGroup resourceGroup)
|
||||
: this()
|
||||
{
|
||||
if (subscription != null)
|
||||
Subscription = subscription;
|
||||
|
||||
if (resourceGroup != null)
|
||||
ResourceGroup = resourceGroup;
|
||||
}
|
||||
|
||||
public List<JObject> Resources { get; }
|
||||
|
||||
public Dictionary<string, object> Parameters { get; }
|
||||
|
||||
public Dictionary<string, object> Variables { get; }
|
||||
|
||||
public CopyIndexStore CopyIndex { get; }
|
||||
|
||||
public ResourceGroup ResourceGroup { get; internal set; }
|
||||
|
||||
public Subscription Subscription { get; internal set; }
|
||||
|
||||
internal void Load(DeploymentParameters parameters)
|
||||
{
|
||||
foreach (var parameter in parameters.Parameters)
|
||||
{
|
||||
if (parameter.Value.Reference != null)
|
||||
{
|
||||
Parameters.Add(parameter.Key, SecretPlaceholder(parameter.Value.Reference.SecretName));
|
||||
}
|
||||
else if (parameter.Value.Value != null)
|
||||
{
|
||||
Parameters.Add(parameter.Key, parameter.Value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string SecretPlaceholder(string secretName)
|
||||
{
|
||||
return string.Concat("{{SecretReference:", secretName, "}}");
|
||||
}
|
||||
|
||||
public sealed class CopyIndexState
|
||||
{
|
||||
internal CopyIndexState()
|
||||
{
|
||||
Index = -1;
|
||||
Count = 1;
|
||||
Input = null;
|
||||
Name = null;
|
||||
}
|
||||
|
||||
internal int Index { get; set; }
|
||||
|
||||
internal int Count { get; set; }
|
||||
|
||||
internal string Name { get; set; }
|
||||
|
||||
internal JToken Input { get; set; }
|
||||
|
||||
internal bool IsCopy()
|
||||
{
|
||||
return Name != null;
|
||||
}
|
||||
|
||||
internal bool Next()
|
||||
{
|
||||
Index++;
|
||||
return Index < Count;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CopyIndexStore
|
||||
{
|
||||
private readonly Dictionary<string, CopyIndexState> _Index;
|
||||
private readonly Stack<CopyIndexState> _Current;
|
||||
|
||||
public CopyIndexStore()
|
||||
{
|
||||
_Index = new Dictionary<string, CopyIndexState>();
|
||||
_Current = new Stack<CopyIndexState>();
|
||||
}
|
||||
|
||||
public CopyIndexState Current
|
||||
{
|
||||
get { return _Current.Count > 0 ? _Current.Peek() : null; }
|
||||
}
|
||||
|
||||
public void Push(CopyIndexState state)
|
||||
{
|
||||
_Current.Push(state);
|
||||
_Index[state.Name] = state;
|
||||
}
|
||||
|
||||
public void Pop()
|
||||
{
|
||||
var state = _Current.Pop();
|
||||
_Index.Remove(state.Name);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string name, out CopyIndexState state)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
state = Current;
|
||||
return state != null;
|
||||
}
|
||||
return _Index.TryGetValue(name, out state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PSObject[] Visit(DeploymentTemplate template, DeploymentParameters parameters)
|
||||
{
|
||||
// Load context
|
||||
_Context = new TemplateContext(_Subscription, _ResourceGroup);
|
||||
_Context.Load(parameters);
|
||||
|
||||
// Process template sections
|
||||
Schema(_Context, template.Schema);
|
||||
ContentVersion(_Context, template.ContentVersion);
|
||||
Parameters(_Context, template.Parameters);
|
||||
Variables(_Context, template.Variables);
|
||||
Resources(_Context, template.Resources);
|
||||
|
||||
// Return results
|
||||
var results = new List<PSObject>();
|
||||
var serializer = new JsonSerializer();
|
||||
serializer.Converters.Add(new PSObjectJsonConverter());
|
||||
foreach (var resource in _Context.Resources)
|
||||
{
|
||||
results.Add(resource.ToObject<PSObject>(serializer));
|
||||
}
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
protected virtual void Schema(TemplateContext context, string schema)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void ContentVersion(TemplateContext context, string contentVersion)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Parameters(TemplateContext context, Dictionary<string, TemplateParameter> parameters)
|
||||
{
|
||||
if (parameters == null || parameters.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
Parameter(context, parameter.Key, parameter.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Parameter(TemplateContext context, string parameterName, TemplateParameter parameter)
|
||||
{
|
||||
if (parameter.DefaultValue == null)
|
||||
return;
|
||||
|
||||
if (!context.Parameters.ContainsKey(parameterName))
|
||||
context.Parameters.Add(parameterName, ExpandProperty<object>(context, parameter.DefaultValue));
|
||||
}
|
||||
|
||||
protected virtual void Functions(TemplateContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Function(TemplateContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Resources(TemplateContext context, IEnumerable<JObject> resources)
|
||||
{
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
Resource(context, resource);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Resource(TemplateContext context, JObject resource)
|
||||
{
|
||||
var condition = !resource.ContainsKey("condition") || ExpandProperty<bool>(context, resource, "condition");
|
||||
if (!condition)
|
||||
return;
|
||||
|
||||
var copyIndex = GetResourceIterator(context, resource);
|
||||
while (copyIndex.Next())
|
||||
{
|
||||
var instance = copyIndex.Input.DeepClone() as JObject;
|
||||
ResourceInstance(context, instance);
|
||||
}
|
||||
if (copyIndex.IsCopy())
|
||||
context.CopyIndex.Pop();
|
||||
}
|
||||
|
||||
protected virtual void ResourceInstance(TemplateContext context, JObject resource)
|
||||
{
|
||||
// Expand resource properties
|
||||
foreach (var property in resource.Properties())
|
||||
ResolveProperty(context, resource, property.Name);
|
||||
|
||||
Emit(context, resource);
|
||||
}
|
||||
|
||||
#region Variables
|
||||
|
||||
protected virtual void Variables(TemplateContext context, Dictionary<string, JToken> variables)
|
||||
{
|
||||
if (variables == null || variables.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var variable in variables)
|
||||
{
|
||||
Variable(context, variable.Key, variable.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Variable(TemplateContext context, string variableName, JToken value)
|
||||
{
|
||||
if (value.Type == JTokenType.Object)
|
||||
{
|
||||
var jobj = value.Value<JObject>();
|
||||
if (TryArrayProperty(jobj, "copy", out JArray copyArray))
|
||||
{
|
||||
var arrayValue = new List<JToken>();
|
||||
foreach (var copy in copyArray)
|
||||
{
|
||||
var copyObject = copy.Value<JObject>();
|
||||
var copyIndex = new TemplateContext.CopyIndexState();
|
||||
copyIndex.Count = ExpandProperty<int>(context, copyObject, "count");
|
||||
copyIndex.Name = ExpandProperty<string>(context, copyObject, "name");
|
||||
copyIndex.Input = copyObject["input"];
|
||||
context.CopyIndex.Push(copyIndex);
|
||||
for (var i = 0; i < copyIndex.Count; i++)
|
||||
{
|
||||
copyIndex.Index = i;
|
||||
var instance = copyIndex.Input.DeepClone();
|
||||
arrayValue.Add(VariableInstance(context, instance));
|
||||
}
|
||||
context.CopyIndex.Pop();
|
||||
jobj.Remove("copy");
|
||||
jobj.Add(copyIndex.Name, new JArray(arrayValue.ToArray()));
|
||||
}
|
||||
}
|
||||
context.Variables.Add(variableName, jobj);
|
||||
return;
|
||||
}
|
||||
context.Variables.Add(variableName, VariableInstance(context, value));
|
||||
}
|
||||
|
||||
private bool TryArrayProperty(JObject obj, string propertyName, out JArray propertyValue)
|
||||
{
|
||||
propertyValue = null;
|
||||
if (!obj.TryGetValue(propertyName, out JToken value) || value.Type != JTokenType.Array)
|
||||
return false;
|
||||
|
||||
propertyValue = value.Value<JArray>();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual JToken VariableInstance(TemplateContext context, JToken value)
|
||||
{
|
||||
if (value.Type == JTokenType.Object)
|
||||
return VariableObject(context, value.Value<JObject>());
|
||||
else if (value.Type == JTokenType.Array)
|
||||
{
|
||||
return VariableArray(context, value.Value<JArray>());
|
||||
}
|
||||
else
|
||||
return VariableSimple(context, value.Value<JValue>());
|
||||
}
|
||||
|
||||
protected virtual JToken VariableObject(TemplateContext context, JObject value)
|
||||
{
|
||||
ExpandObjectInstance2(context, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
protected virtual JToken VariableArray(TemplateContext context, JArray value)
|
||||
{
|
||||
return ExpandArray(context, value);
|
||||
}
|
||||
|
||||
protected virtual JToken VariableSimple(TemplateContext context, JValue value)
|
||||
{
|
||||
var result = ExpandProperty<object>(context, value.Value<string>());
|
||||
return result == null ? JValue.CreateNull() : JToken.FromObject(result);
|
||||
}
|
||||
|
||||
#endregion Variables
|
||||
|
||||
protected virtual void Outputs()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected StringExpression<T> Expression<T>(TemplateContext context, string s)
|
||||
{
|
||||
var builder = new ExpressionBuilder();
|
||||
return () => EvaluateExpression<T>(builder.Build(s));
|
||||
}
|
||||
|
||||
private T EvaluateExpression<T>(ExpressionFnOuter fn)
|
||||
{
|
||||
return (T)fn(_Context);
|
||||
}
|
||||
|
||||
private T ExpandProperty<T>(TemplateContext context, object value)
|
||||
{
|
||||
return value is string s && IsExpressionString(s) ? EvaluteExpression<T>(context, s) : (T)value;
|
||||
}
|
||||
|
||||
private T ExpandProperty<T>(TemplateContext context, JToken value)
|
||||
{
|
||||
if (value.Type != JTokenType.String)
|
||||
return value.Value<T>();
|
||||
|
||||
var svalue = value.Value<string>();
|
||||
return IsExpressionString(svalue) ? EvaluteExpression<T>(context, svalue) : value.Value<T>();
|
||||
}
|
||||
|
||||
private JToken ExpandPropertyToken(TemplateContext context, JToken value)
|
||||
{
|
||||
if (!TryExpressionString(value, out string svalue))
|
||||
return value;
|
||||
|
||||
return JToken.FromObject(EvaluteExpression<object>(context, svalue));
|
||||
}
|
||||
|
||||
private T ExpandProperty<T>(TemplateContext context, JObject value, string propertyName)
|
||||
{
|
||||
if (!value.ContainsKey(propertyName))
|
||||
return default(T);
|
||||
|
||||
var propertyValue = value[propertyName].Value<JValue>().Value;
|
||||
return (propertyValue is string svalue) ? ExpandProperty<T>(context, propertyValue) : (T)propertyValue;
|
||||
}
|
||||
|
||||
private void ResolvePropertyExpression(TemplateContext context, JObject value, string propertyName)
|
||||
{
|
||||
if (!value.ContainsKey(propertyName))
|
||||
return;
|
||||
|
||||
value[propertyName] = ExpandProperty<string>(context, value, propertyName);
|
||||
}
|
||||
|
||||
private void ResolveProperty(TemplateContext context, JObject obj, string propertyName)
|
||||
{
|
||||
if (!obj.ContainsKey(propertyName))
|
||||
return;
|
||||
|
||||
var value = obj[propertyName];
|
||||
if (value is JObject jObject)
|
||||
{
|
||||
var copyIndex = GetPropertyIterator(context, jObject);
|
||||
if (copyIndex.IsCopy())
|
||||
{
|
||||
while (copyIndex.Next())
|
||||
{
|
||||
var instance = copyIndex.Input.DeepClone();
|
||||
jObject[copyIndex.Name] = ResolveToken(context, instance);
|
||||
}
|
||||
context.CopyIndex.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
obj[propertyName] = ResolveToken(context, jObject);
|
||||
}
|
||||
}
|
||||
else if (value is JArray jArray)
|
||||
{
|
||||
obj[propertyName] = ExpandArray(context, jArray);
|
||||
}
|
||||
else if (value is JToken jToken && jToken.Type == JTokenType.String)
|
||||
{
|
||||
obj[propertyName] = ExpandPropertyToken(context, jToken);
|
||||
}
|
||||
}
|
||||
|
||||
private JToken ResolveToken(TemplateContext context, JToken token)
|
||||
{
|
||||
if (token is JObject jObject)
|
||||
{
|
||||
return ExpandObject2(context, jObject);
|
||||
}
|
||||
else if (token is JArray jArray)
|
||||
{
|
||||
return ExpandArray(context, jArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ExpandPropertyToken(context, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a property based iterator copy.
|
||||
/// </summary>
|
||||
private TemplateContext.CopyIndexState GetPropertyIterator(TemplateContext context, JObject value)
|
||||
{
|
||||
var result = new TemplateContext.CopyIndexState();
|
||||
result.Input = value;
|
||||
if (value.ContainsKey("copy"))
|
||||
{
|
||||
var copyObject = value["copy"].Value<JObject>();
|
||||
result.Name = ExpandProperty<string>(context, copyObject, "name");
|
||||
result.Input = copyObject["input"];
|
||||
result.Count = ExpandProperty<int>(context, copyObject, "count");
|
||||
context.CopyIndex.Push(result);
|
||||
value.Remove("copy");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource based iterator copy.
|
||||
/// </summary>
|
||||
private TemplateContext.CopyIndexState GetResourceIterator(TemplateContext context, JObject value)
|
||||
{
|
||||
var result = new TemplateContext.CopyIndexState();
|
||||
result.Input = value;
|
||||
if (value.ContainsKey("copy"))
|
||||
{
|
||||
var copyObject = value["copy"].Value<JObject>();
|
||||
result.Name = ExpandProperty<string>(context, copyObject, "name");
|
||||
result.Count = ExpandProperty<int>(context, copyObject, "count");
|
||||
context.CopyIndex.Push(result);
|
||||
value.Remove("copy");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private JToken ExpandObject2(TemplateContext context, JObject obj)
|
||||
{
|
||||
var copyIndex = GetPropertyIterator(context, obj);
|
||||
if (copyIndex.IsCopy())
|
||||
{
|
||||
var array = new JArray();
|
||||
while (copyIndex.Next())
|
||||
{
|
||||
var instance = copyIndex.Input.DeepClone();
|
||||
array.Add(ResolveToken(context, instance));
|
||||
}
|
||||
obj[copyIndex.Name] = array;
|
||||
context.CopyIndex.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var property in obj.Properties())
|
||||
{
|
||||
ResolveProperty(context, obj, property.Name);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private void ExpandObjectInstance2(TemplateContext context, JObject obj)
|
||||
{
|
||||
foreach (var property in obj.Properties())
|
||||
{
|
||||
ResolveProperty(context, obj, property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private JToken ExpandArray(TemplateContext context, JArray array)
|
||||
{
|
||||
var result = new JArray();
|
||||
|
||||
for (var i = 0; i < array.Count; i++)
|
||||
{
|
||||
if (array[i] is JObject jObject)
|
||||
{
|
||||
result.Add(ExpandObject2(context, jObject));
|
||||
}
|
||||
else if (array[i] is JArray jArray)
|
||||
{
|
||||
result.Add(ExpandArray(context, jArray));
|
||||
}
|
||||
else if (array[i] is JToken jToken && jToken.Type == JTokenType.String)
|
||||
{
|
||||
result.Add(ExpandPropertyToken(context, jToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(array[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected T EvaluteExpression<T>(TemplateContext context, string value)
|
||||
{
|
||||
var exp = Expression<T>(context, value);
|
||||
return exp();
|
||||
}
|
||||
|
||||
protected bool IsExpressionString(string value)
|
||||
{
|
||||
return value != null &&
|
||||
value.Length >= 5 && // [f()]
|
||||
value[0] == '[' &&
|
||||
value[value.Length - 1] == ']';
|
||||
}
|
||||
|
||||
protected bool TryExpressionString(JToken token, out string value)
|
||||
{
|
||||
value = null;
|
||||
if (token.Type != JTokenType.String)
|
||||
return false;
|
||||
|
||||
value = token.Value<string>();
|
||||
return IsExpressionString(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit a resource object.
|
||||
/// </summary>
|
||||
protected void Emit(TemplateContext context, JObject resource)
|
||||
{
|
||||
context.Resources.Add(resource);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A template visitor for generating rule data.
|
||||
/// </summary>
|
||||
internal sealed class RuleDataExportVisitor : TemplateVisitor
|
||||
{
|
||||
internal RuleDataExportVisitor(Subscription subscription, ResourceGroup resourceGroup)
|
||||
: base (subscription, resourceGroup) { }
|
||||
|
||||
protected override void ResourceInstance(TemplateContext context, JObject resource)
|
||||
{
|
||||
// Remove resource properties that not required in rule data
|
||||
if (resource.ContainsKey("apiVersion"))
|
||||
resource.Remove("apiVersion");
|
||||
|
||||
if (resource.ContainsKey("condition"))
|
||||
resource.Remove("condition");
|
||||
|
||||
if (resource.ContainsKey("comments"))
|
||||
resource.Remove("comments");
|
||||
|
||||
if (resource.ContainsKey("dependsOn"))
|
||||
resource.Remove("dependsOn");
|
||||
|
||||
base.ResourceInstance(context, resource);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
public enum ExpressionTokenType : byte
|
||||
{
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// A function name.
|
||||
/// </summary>
|
||||
Element,
|
||||
|
||||
Property,
|
||||
|
||||
/// <summary>
|
||||
/// A string literal.
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// A numeric literal.
|
||||
/// </summary>
|
||||
Numeric,
|
||||
|
||||
/// <summary>
|
||||
/// Start a grouping '('.
|
||||
/// </summary>
|
||||
GroupStart,
|
||||
|
||||
/// <summary>
|
||||
/// End a grouping ')'.
|
||||
/// </summary>
|
||||
GroupEnd,
|
||||
|
||||
/// <summary>
|
||||
/// Start an index '['.
|
||||
/// </summary>
|
||||
IndexStart,
|
||||
|
||||
/// <summary>
|
||||
/// End an index ']'.
|
||||
/// </summary>
|
||||
IndexEnd
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An individual expression token.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Type = {Type}, Content = {Content}")]
|
||||
internal sealed class ExpressionToken
|
||||
{
|
||||
internal readonly ExpressionTokenType Type;
|
||||
internal readonly string Content;
|
||||
|
||||
internal ExpressionToken(ExpressionTokenType type, string content)
|
||||
{
|
||||
Type = type;
|
||||
Content = content;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an expression token to a token stream.
|
||||
/// </summary>
|
||||
internal static class TokenStreamExtensions
|
||||
{
|
||||
internal static void Function(this TokenStream stream, string functionName)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.Element, functionName));
|
||||
}
|
||||
|
||||
internal static void String(this TokenStream stream, string s)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.String, s));
|
||||
}
|
||||
|
||||
internal static void Property(this TokenStream stream, string propertyName)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.Property, propertyName));
|
||||
}
|
||||
|
||||
internal static void GroupStart(this TokenStream stream)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.GroupStart, null));
|
||||
}
|
||||
|
||||
internal static void GroupEnd(this TokenStream stream)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.GroupEnd, null));
|
||||
}
|
||||
|
||||
internal static void IndexStart(this TokenStream stream)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.IndexStart, null));
|
||||
}
|
||||
|
||||
internal static void IndexEnd(this TokenStream stream)
|
||||
{
|
||||
stream.Add(new ExpressionToken(ExpressionTokenType.IndexEnd, null));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stream of template expression tokens.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Current = (Type = {Current.Type}, Content = {Current.Content})")]
|
||||
internal sealed class TokenStream
|
||||
{
|
||||
private readonly List<ExpressionToken> _Token;
|
||||
|
||||
private int _Position;
|
||||
|
||||
internal TokenStream()
|
||||
{
|
||||
_Token = new List<ExpressionToken>();
|
||||
}
|
||||
|
||||
internal TokenStream(IEnumerable<ExpressionToken> tokens)
|
||||
: this()
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
Add(token);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
public ExpressionToken Current
|
||||
{
|
||||
get
|
||||
{
|
||||
return (_Token.Count <= _Position) ? null : _Token[_Position];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { return _Token.Count; }
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
public bool TryTokenType(ExpressionTokenType tokenType, out ExpressionToken token)
|
||||
{
|
||||
token = null;
|
||||
if (Current == null || Current.Type != tokenType)
|
||||
return false;
|
||||
|
||||
token = Pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Skip(ExpressionTokenType tokenType)
|
||||
{
|
||||
if (Current == null || Current.Type != tokenType)
|
||||
return false;
|
||||
|
||||
Pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Add(ExpressionToken token)
|
||||
{
|
||||
_Token.Add(token);
|
||||
_Position = _Token.Count - 1;
|
||||
}
|
||||
|
||||
public ExpressionToken Pop()
|
||||
{
|
||||
if (Count == 0)
|
||||
return null;
|
||||
|
||||
var token = _Token[_Position];
|
||||
_Token.RemoveAt(_Position);
|
||||
return token;
|
||||
}
|
||||
|
||||
public void MoveTo(int position)
|
||||
{
|
||||
_Position = position;
|
||||
}
|
||||
|
||||
internal ExpressionToken[] ToArray()
|
||||
{
|
||||
return _Token.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PSRule.Rules.Azure.Data.Template
|
||||
{
|
||||
public sealed class Subscription
|
||||
{
|
||||
private const string DEFAULT_ID = "/subscriptions/{{Subscription.SubscriptionId}}";
|
||||
private const string DEFAULT_SUBSCRIPTIONID = "{{Subscription.SubscriptionId}}";
|
||||
private const string DEFAULT_TENANTID = "{{Subscription.TenantId}}";
|
||||
private const string DEFAULT_DISPLAYNAME = "{{Subscription.Name}}";
|
||||
|
||||
internal readonly static Subscription Default = new Subscription();
|
||||
|
||||
internal Subscription()
|
||||
{
|
||||
SubscriptionId = DEFAULT_SUBSCRIPTIONID;
|
||||
TenantId = DEFAULT_TENANTID;
|
||||
DisplayName = DEFAULT_DISPLAYNAME;
|
||||
Id = DEFAULT_ID;
|
||||
}
|
||||
|
||||
internal Subscription(string subscriptionId, string tenantId, string displayName)
|
||||
{
|
||||
SubscriptionId = subscriptionId;
|
||||
TenantId = tenantId;
|
||||
DisplayName = displayName;
|
||||
Id = string.Concat("/subscriptions/", SubscriptionId);
|
||||
}
|
||||
|
||||
public readonly string Id;
|
||||
|
||||
public readonly string SubscriptionId;
|
||||
|
||||
public readonly string TenantId;
|
||||
|
||||
public readonly string DisplayName;
|
||||
}
|
||||
|
||||
public sealed class ResourceGroup
|
||||
{
|
||||
private const string DEFAULT_ID = "/subscriptions/{{Subscription.SubscriptionId}}/resourceGroups/{{ResourceGroup.Name}}";
|
||||
private const string DEFAULT_NAME = "{{ResourceGroup.Name}}";
|
||||
private const string DEFAULT_TYPE = "Microsoft.Resources/resourceGroups";
|
||||
private const string DEFAULT_LOCATION = "{{ResourceGroup.Location}}";
|
||||
private const string DEFAULT_MANAGEDBY = "{{ResourceGroup.ManagedBy}}";
|
||||
private const Hashtable DEFAULT_TAGS = null;
|
||||
private const string DEFAULT_PROVISIONINGSTATE = "Succeeded";
|
||||
|
||||
internal readonly static ResourceGroup Default = new ResourceGroup();
|
||||
|
||||
internal ResourceGroup()
|
||||
{
|
||||
Id = DEFAULT_ID;
|
||||
Name = DEFAULT_NAME;
|
||||
Type = DEFAULT_TYPE;
|
||||
Location = DEFAULT_LOCATION;
|
||||
ManagedBy = DEFAULT_MANAGEDBY;
|
||||
Tags = DEFAULT_TAGS;
|
||||
Properties = new ResourceGroupProperties(DEFAULT_PROVISIONINGSTATE);
|
||||
}
|
||||
|
||||
internal ResourceGroup(string id, string name, string location, string managedBy, Hashtable tags)
|
||||
: this()
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Location = location;
|
||||
ManagedBy = managedBy;
|
||||
Tags = tags;
|
||||
}
|
||||
|
||||
public sealed class ResourceGroupProperties
|
||||
{
|
||||
public readonly string ProvisioningState;
|
||||
|
||||
internal ResourceGroupProperties(string provisioningState)
|
||||
{
|
||||
ProvisioningState = provisioningState;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly string Id;
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
public readonly string Type;
|
||||
|
||||
public readonly string Location;
|
||||
|
||||
public readonly string ManagedBy;
|
||||
|
||||
public readonly Hashtable Tags;
|
||||
|
||||
public readonly ResourceGroupProperties Properties;
|
||||
}
|
||||
|
||||
public sealed class MockResource
|
||||
{
|
||||
public MockResource()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MockResourceList
|
||||
{
|
||||
public MockResourceList()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DeploymentTemplate
|
||||
{
|
||||
public sealed class TemplateResourceSku
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public sealed class TemplateParameter
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
public JToken DefaultValue { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TemplateResource
|
||||
{
|
||||
public string Condition { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string ApiVersion { get; set; }
|
||||
|
||||
public string Location { get; set; }
|
||||
|
||||
public TemplateResourceSku Sku { get; set; }
|
||||
|
||||
public string Kind { get; set; }
|
||||
|
||||
public Dictionary<string, object> Properties { get; set; }
|
||||
|
||||
public TemplateResourceCopy Copy { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("$schema")]
|
||||
public string Schema { get; set; }
|
||||
|
||||
[JsonProperty("contentVersion")]
|
||||
public string ContentVersion { get; set; }
|
||||
|
||||
public Dictionary<string, TemplateParameter> Parameters { get; set; }
|
||||
|
||||
[JsonProperty("variables")]
|
||||
public Dictionary<string, JToken> Variables { get; set; }
|
||||
|
||||
//public Dictionary<string, object> Functions { get; set; }
|
||||
|
||||
[JsonProperty("resources")]
|
||||
public List<JObject> Resources { get; set; }
|
||||
|
||||
[JsonProperty("outputs")]
|
||||
public Dictionary<string, object> Outputs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TemplateResourceCopy
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
|
||||
public string Mode { get; set; }
|
||||
|
||||
public int BatchSize { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DeploymentParameters
|
||||
{
|
||||
[JsonProperty("$schema")]
|
||||
public string Schema { get; set; }
|
||||
|
||||
[JsonProperty("contentVersion")]
|
||||
public string ContentVersion { get; set; }
|
||||
|
||||
[JsonProperty("parameters")]
|
||||
public Dictionary<string, DeploymentParameterValue> Parameters { get; set; }
|
||||
|
||||
public sealed class DeploymentParameterValue
|
||||
{
|
||||
[JsonProperty("value")]
|
||||
public object Value { get; set; }
|
||||
|
||||
public DeploymentParameterKeyVaultReference Reference {get;set;}
|
||||
}
|
||||
|
||||
public class DeploymentParameterKeyVaultReference
|
||||
{
|
||||
public string SecretName { get; set; }
|
||||
|
||||
public ResourceReference KeyVault { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ResourceReference
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.27.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources\PSRuleResources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>PSRuleResources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources\PSRuleResources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>PSRuleResources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -56,7 +56,9 @@ RequiredModules = @(
|
|||
)
|
||||
|
||||
# Assemblies that must be loaded prior to importing this module
|
||||
# RequiredAssemblies = @()
|
||||
RequiredAssemblies = @(
|
||||
'PSRule.Rules.Azure.dll'
|
||||
)
|
||||
|
||||
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
|
||||
# ScriptsToProcess = @()
|
||||
|
@ -73,6 +75,7 @@ RequiredModules = @(
|
|||
# Functions 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 functions to export.
|
||||
FunctionsToExport = @(
|
||||
'Export-AzRuleData'
|
||||
'Export-AzTemplateRuleData'
|
||||
)
|
||||
|
||||
# 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.
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
Set-StrictMode -Version latest;
|
||||
|
||||
[PSRule.Rules.Azure.Configuration.PSRuleOption]::UseExecutionContext($ExecutionContext);
|
||||
|
||||
#
|
||||
# Localization
|
||||
#
|
||||
|
@ -45,7 +47,6 @@ function Export-AzRuleData {
|
|||
[Parameter(Mandatory = $False, ParameterSetName = 'All')]
|
||||
[Switch]$All = $False
|
||||
)
|
||||
|
||||
process {
|
||||
# Get subscriptions
|
||||
$context = FindAzureContext -Subscription $Subscription -Tenant $Tenant -All:$All -Verbose:$VerbosePreference;
|
||||
|
@ -80,12 +81,118 @@ function Export-AzRuleData {
|
|||
}
|
||||
}
|
||||
|
||||
# .ExternalHelp PSRule.Rules.Azure-Help.xml
|
||||
function Export-AzTemplateRuleData {
|
||||
[CmdletBinding()]
|
||||
[OutputType([System.IO.FileInfo])]
|
||||
[OutputType([PSObject])]
|
||||
param (
|
||||
[Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
|
||||
[String]$TemplateFile,
|
||||
|
||||
[Parameter(Mandatory = $False, ValueFromPipelineByPropertyName = $True)]
|
||||
[Alias('TemplateParameterFile')]
|
||||
[String[]]$ParameterFile,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[String]$ResourceGroupName,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[String]$Subscription,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[String]$OutputPath = $PWD,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[Switch]$PassThru
|
||||
)
|
||||
begin {
|
||||
Write-Verbose -Message '[Export-AzTemplateRuleData] BEGIN::';
|
||||
|
||||
# Build the pipeline
|
||||
$builder = [PSRule.Rules.Azure.Pipeline.PipelineBuilder]::Template();
|
||||
$builder.PassThru($PassThru);
|
||||
$builder.OutputPath($OutputPath);
|
||||
|
||||
# Bind to subscription context
|
||||
if ($PSBoundParameters.ContainsKey('Subscription')) {
|
||||
$subscriptionObject = GetSubscription -Subscription $Subscription -ErrorAction SilentlyContinue;
|
||||
if ($Null -ne $subscriptionObject) {
|
||||
$builder.Subscription($subscriptionObject);
|
||||
}
|
||||
}
|
||||
# Bind to resource group
|
||||
if ($PSBoundParameters.ContainsKey('ResourceGroupName')) {
|
||||
$resourceGroupObject = GetResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue;
|
||||
if ($Null -ne $resourceGroupObject) {
|
||||
$builder.ResourceGroup($resourceGroupObject);
|
||||
}
|
||||
}
|
||||
|
||||
$builder.UseCommandRuntime($PSCmdlet.CommandRuntime);
|
||||
$builder.UseExecutionContext($ExecutionContext);
|
||||
try {
|
||||
$pipeline = $builder.Build();
|
||||
$pipeline.Begin();
|
||||
}
|
||||
catch {
|
||||
$pipeline.Dispose();
|
||||
}
|
||||
}
|
||||
process {
|
||||
|
||||
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
$source = [PSRule.Rules.Azure.Pipeline.TemplateSource]::new($TemplateFile, $ParameterFile);
|
||||
$pipeline.Process($source);
|
||||
}
|
||||
catch {
|
||||
$pipeline.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
end {
|
||||
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
$pipeline.End();
|
||||
}
|
||||
finally {
|
||||
$pipeline.Dispose();
|
||||
}
|
||||
}
|
||||
Write-Verbose -Message '[Export-AzTemplateRuleData] END::';
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Public functions
|
||||
|
||||
#
|
||||
# Helper functions
|
||||
#
|
||||
|
||||
function GetResourceGroup {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $True)]
|
||||
[String]$Name
|
||||
)
|
||||
process {
|
||||
return Get-AzResourceGroup -Name $Name -ErrorAction SilentlyContinue;
|
||||
}
|
||||
}
|
||||
|
||||
function GetSubscription {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $True)]
|
||||
[String]$Subscription
|
||||
)
|
||||
process {
|
||||
return (Set-AzContext -Subscription $Subscription -ErrorAction SilentlyContinue).Subscription;
|
||||
}
|
||||
}
|
||||
|
||||
function FindAzureContext {
|
||||
[CmdletBinding()]
|
||||
[OutputType([Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer[]])]
|
||||
|
@ -502,4 +609,4 @@ function SetResourceType {
|
|||
# Export module
|
||||
#
|
||||
|
||||
Export-ModuleMember -Function 'Export-AzRuleData';
|
||||
Export-ModuleMember -Function 'Export-AzRuleData', 'Export-AzTemplateRuleData';
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security.Permissions;
|
||||
|
||||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for all pipeline exceptions.
|
||||
/// </summary>
|
||||
public abstract class PipelineException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a pipeline exception.
|
||||
/// </summary>
|
||||
protected PipelineException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pipeline exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The detail of the exception.</param>
|
||||
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(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A serialization exception.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class PipelineSerializationException : PipelineException
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a serialization exception.
|
||||
/// </summary>
|
||||
public PipelineSerializationException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serialization exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The detail of the exception.</param>
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
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("info");
|
||||
base.GetObjectData(info, context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
using PSRule.Rules.Azure.Configuration;
|
||||
using PSRule.Rules.Azure.Resources;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Text;
|
||||
|
||||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
internal delegate bool ShouldProcess(string target, string action);
|
||||
|
||||
/// <summary>
|
||||
/// A helper class for building a pipeline from PowerShell.
|
||||
/// </summary>
|
||||
public static class PipelineBuilder
|
||||
{
|
||||
public static ITemplatePipelineBuilder Template()
|
||||
{
|
||||
return new TemplatePipelineBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPipelineBuilder
|
||||
{
|
||||
void UseCommandRuntime(ICommandRuntime2 commandRuntime);
|
||||
|
||||
void UseExecutionContext(EngineIntrinsics executionContext);
|
||||
|
||||
IPipelineBuilder Configure(PSRuleOption option);
|
||||
|
||||
IPipeline Build();
|
||||
}
|
||||
|
||||
public interface IPipeline
|
||||
{
|
||||
void Begin();
|
||||
|
||||
void Process(PSObject sourceObject);
|
||||
|
||||
void End();
|
||||
}
|
||||
|
||||
internal abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
{
|
||||
protected readonly PSRuleOption Option;
|
||||
|
||||
protected WriteOutput Output;
|
||||
protected ShouldProcess ShouldProcess;
|
||||
|
||||
protected PipelineBuilderBase()
|
||||
{
|
||||
Option = new PSRuleOption();
|
||||
}
|
||||
|
||||
public virtual void UseCommandRuntime(ICommandRuntime2 commandRuntime)
|
||||
{
|
||||
//Logger.UseCommandRuntime(commandRuntime);
|
||||
Output = commandRuntime.WriteObject;
|
||||
ShouldProcess = commandRuntime.ShouldProcess;
|
||||
}
|
||||
|
||||
public void UseExecutionContext(EngineIntrinsics executionContext)
|
||||
{
|
||||
//Logger.UseExecutionContext(executionContext);
|
||||
}
|
||||
|
||||
public virtual IPipelineBuilder Configure(PSRuleOption option)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract IPipeline Build();
|
||||
|
||||
protected PipelineContext PrepareContext()
|
||||
{
|
||||
return new PipelineContext(Option);
|
||||
}
|
||||
|
||||
protected virtual PipelineWriter PrepareWriter()
|
||||
{
|
||||
return new PowerShellWriter(Output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write output to file.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to write.</param>
|
||||
/// <param name="encoding">The file encoding to use.</param>
|
||||
/// <param name="o">The text to write.</param>
|
||||
protected static void WriteToFile(string path, ShouldProcess shouldProcess, Encoding encoding, object o)
|
||||
{
|
||||
var rootedPath = PSRuleOption.GetRootedPath(path: path);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class PipelineBase : IDisposable, IPipeline
|
||||
{
|
||||
protected readonly PipelineContext Context;
|
||||
|
||||
// Track whether Dispose has been called.
|
||||
private bool _Disposed = false;
|
||||
|
||||
|
||||
protected PipelineBase(PipelineContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
#region IPipeline
|
||||
|
||||
public virtual void Begin()
|
||||
{
|
||||
//Reader.Open();
|
||||
}
|
||||
|
||||
public virtual void Process(PSObject sourceObject)
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public virtual void End()
|
||||
{
|
||||
//Writer.End();
|
||||
}
|
||||
|
||||
#endregion IPipeline
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_Disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
//Context.Dispose();
|
||||
}
|
||||
_Disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IDisposable
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using PSRule.Rules.Azure.Configuration;
|
||||
|
||||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
internal sealed class PipelineContext
|
||||
{
|
||||
internal readonly PSRuleOption Option;
|
||||
|
||||
public PipelineContext(PSRuleOption option)
|
||||
{
|
||||
Option = option;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
internal delegate void WriteOutput(object o, bool enumerate);
|
||||
|
||||
internal abstract class PipelineWriter
|
||||
{
|
||||
private readonly WriteOutput _Output;
|
||||
|
||||
protected PipelineWriter(WriteOutput output)
|
||||
{
|
||||
_Output = output;
|
||||
}
|
||||
|
||||
public virtual void Write(object o, bool enumerate)
|
||||
{
|
||||
_Output(o, enumerate);
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
_Result.Add(o);
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
WriteObjectJson();
|
||||
}
|
||||
|
||||
private void WriteObjectJson()
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
settings.Converters.Add(new PSObjectJsonConverter());
|
||||
var json = JsonConvert.SerializeObject(_Result.ToArray(), settings: settings);
|
||||
base.Write(json, false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
using Newtonsoft.Json;
|
||||
using PSRule.Rules.Azure.Configuration;
|
||||
using PSRule.Rules.Azure.Data.Template;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Text;
|
||||
|
||||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
public interface ITemplatePipelineBuilder : IPipelineBuilder
|
||||
{
|
||||
void ResourceGroup(PSObject resourceGroup);
|
||||
|
||||
void Subscription(PSObject subscription);
|
||||
|
||||
void PassThru(bool passThru);
|
||||
|
||||
void OutputPath(string outputPath);
|
||||
}
|
||||
|
||||
internal sealed class TemplatePipelineBuilder : PipelineBuilderBase, ITemplatePipelineBuilder
|
||||
{
|
||||
private Subscription _Subscription;
|
||||
private ResourceGroup _ResourceGroup;
|
||||
private bool _PassThru;
|
||||
private string _OutputPath;
|
||||
|
||||
internal TemplatePipelineBuilder()
|
||||
: base()
|
||||
{
|
||||
_Subscription = Data.Template.Subscription.Default;
|
||||
_ResourceGroup = Data.Template.ResourceGroup.Default;
|
||||
}
|
||||
|
||||
public void ResourceGroup(PSObject resourceGroup)
|
||||
{
|
||||
_ResourceGroup = new ResourceGroup(
|
||||
id: GetProperty<string>(resourceGroup, "ResourceId"),
|
||||
name: GetProperty<string>(resourceGroup, "ResourceGroupName"),
|
||||
location: GetProperty<string>(resourceGroup, "Location"),
|
||||
managedBy: GetProperty<string>(resourceGroup, "ManagedBy"),
|
||||
tags: GetProperty<Hashtable>(resourceGroup, "Tags")
|
||||
);
|
||||
}
|
||||
|
||||
public void Subscription(PSObject subscription)
|
||||
{
|
||||
_Subscription = new Subscription(
|
||||
subscriptionId: GetProperty<string>(subscription, "SubscriptionId"),
|
||||
tenantId: GetProperty<string>(subscription, "TenantId"),
|
||||
displayName: GetProperty<string>(subscription, "Name")
|
||||
);
|
||||
}
|
||||
|
||||
public void PassThru(bool passThru)
|
||||
{
|
||||
_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 PrepareWriter()
|
||||
{
|
||||
WriteOutput output = (o, enumerate) => WriteToFile(_OutputPath, ShouldProcess, Encoding.UTF8, o);
|
||||
return _PassThru ? base.PrepareWriter() : new JsonPipelineWriter(output);
|
||||
}
|
||||
|
||||
public override IPipeline Build()
|
||||
{
|
||||
return new TemplatePipeline(PrepareContext(), PrepareWriter(), _ResourceGroup, _Subscription);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TemplatePipeline : PipelineBase
|
||||
{
|
||||
private readonly PipelineWriter _Writer;
|
||||
private readonly ResourceGroup _ResourceGroup;
|
||||
private readonly Subscription _Subscription;
|
||||
|
||||
internal TemplatePipeline(PipelineContext context, PipelineWriter writer, ResourceGroup resourceGroup, Subscription subscription)
|
||||
: base(context)
|
||||
{
|
||||
_Writer = writer;
|
||||
_ResourceGroup = resourceGroup;
|
||||
_Subscription = subscription;
|
||||
}
|
||||
|
||||
public override void Process(PSObject sourceObject)
|
||||
{
|
||||
if (sourceObject == null || !(sourceObject.BaseObject is TemplateSource source))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < source.ParametersFile.Length; i++)
|
||||
{
|
||||
var output = ProcessTemplate(source.TemplateFile, source.ParametersFile[i]);
|
||||
_Writer.Write(output, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
_Writer.End();
|
||||
}
|
||||
|
||||
internal PSObject[] ProcessTemplate(string templateFile, string parametersFile)
|
||||
{
|
||||
var templateObject = ReadFile<DeploymentTemplate>(PSRuleOption.GetRootedPath(templateFile));
|
||||
if (templateObject == null)
|
||||
throw new FileNotFoundException();
|
||||
|
||||
var parametersObject = ReadFile<DeploymentParameters>(PSRuleOption.GetRootedPath(parametersFile));
|
||||
var visitor = new RuleDataExportVisitor(_Subscription, _ResourceGroup);
|
||||
return visitor.Visit(templateObject, parametersObject);
|
||||
}
|
||||
|
||||
private static T ReadFile<T>(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
return default(T);
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
namespace PSRule.Rules.Azure.Pipeline
|
||||
{
|
||||
public sealed class TemplateSource
|
||||
{
|
||||
public readonly string TemplateFile;
|
||||
public readonly string[] ParametersFile;
|
||||
|
||||
public TemplateSource(string templateFile, string[] parametersFile)
|
||||
{
|
||||
TemplateFile = templateFile;
|
||||
ParametersFile = parametersFile;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("PSRule.Rules.Azure.Tests")]
|
|
@ -0,0 +1,126 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <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 PSRuleResources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal PSRuleResources() {
|
||||
}
|
||||
|
||||
/// <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.PSRuleResources", typeof(PSRuleResources).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 The specified parameter is not a valid integer..
|
||||
/// </summary>
|
||||
internal static string FunctionInvalidInteger {
|
||||
get {
|
||||
return ResourceManager.GetString("FunctionInvalidInteger", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The specified parameter is not a valid string..
|
||||
/// </summary>
|
||||
internal static string FunctionInvalidString {
|
||||
get {
|
||||
return ResourceManager.GetString("FunctionInvalidString", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The function "{0}" was not found..
|
||||
/// </summary>
|
||||
internal static string FunctionNotFound {
|
||||
get {
|
||||
return ResourceManager.GetString("FunctionNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Read JSON failed..
|
||||
/// </summary>
|
||||
internal static string ReadJsonFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ReadJsonFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Can not serialize a null PSObject..
|
||||
/// </summary>
|
||||
internal static string SerializeNullPSObject {
|
||||
get {
|
||||
return ResourceManager.GetString("SerializeNullPSObject", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create path.
|
||||
/// </summary>
|
||||
internal static string ShouldCreatePath {
|
||||
get {
|
||||
return ResourceManager.GetString("ShouldCreatePath", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Write file.
|
||||
/// </summary>
|
||||
internal static string ShouldWriteFile {
|
||||
get {
|
||||
return ResourceManager.GetString("ShouldWriteFile", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
<?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="FunctionInvalidInteger" xml:space="preserve">
|
||||
<value>The specified parameter is not a valid integer.</value>
|
||||
</data>
|
||||
<data name="FunctionInvalidString" xml:space="preserve">
|
||||
<value>The specified parameter is not a valid string.</value>
|
||||
</data>
|
||||
<data name="FunctionNotFound" xml:space="preserve">
|
||||
<value>The function "{0}" was not found.</value>
|
||||
</data>
|
||||
<data name="ReadJsonFailed" xml:space="preserve">
|
||||
<value>Read JSON failed.</value>
|
||||
</data>
|
||||
<data name="SerializeNullPSObject" xml:space="preserve">
|
||||
<value>Can not serialize a null PSObject.</value>
|
||||
</data>
|
||||
<data name="ShouldCreatePath" xml:space="preserve">
|
||||
<value>Create path</value>
|
||||
</data>
|
||||
<data name="ShouldWriteFile" xml:space="preserve">
|
||||
<value>Write file</value>
|
||||
</data>
|
||||
</root>
|
|
@ -67,7 +67,7 @@ Rule 'Azure.VM.UniqueDns' -Type 'Microsoft.Network/networkInterfaces' -Tag @{ se
|
|||
}
|
||||
|
||||
# Synopsis: Managed disks should be attached to virtual machines
|
||||
Rule 'Azure.VM.DiskAttached' -If { (ResourceType 'Microsoft.Compute/disks') -and ($TargetObject.ResourceName -notlike '*-ASRReplica') } -Tag @{ severity = 'Awareness'; category = 'Operations management' } {
|
||||
Rule 'Azure.VM.DiskAttached' -Type 'Microsoft.Compute/disks' -If { ($TargetObject.ResourceName -notlike '*-ASRReplica') } -Tag @{ severity = 'Awareness'; category = 'Operations management' } {
|
||||
# Disks should be attached unless they are used by ASR, which are not attached until fail over
|
||||
# Disks for VMs that are off are marked as Reserved
|
||||
Within 'properties.diskState' 'Attached', 'Reserved'
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#region Virtual Network
|
||||
|
||||
# Synopsis: Subnets should have NSGs assigned, except for the GatewaySubnet
|
||||
Rule 'Azure.VirtualNetwork.UseNSGs' -If { ResourceType 'Microsoft.Network/virtualNetworks' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Rule 'Azure.VirtualNetwork.UseNSGs' -Type 'Microsoft.Network/virtualNetworks' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Recommend 'Subnets should have NSGs assigned'
|
||||
|
||||
# Get subnets
|
||||
|
@ -22,7 +22,7 @@ Rule 'Azure.VirtualNetwork.UseNSGs' -If { ResourceType 'Microsoft.Network/virtua
|
|||
# TODO: Check that NSG on GatewaySubnet is not defined
|
||||
|
||||
# Synopsis: VNETs should have at least two DNS servers assigned
|
||||
Rule 'Azure.VirtualNetwork.SingleDNS' -If { ResourceType 'Microsoft.Network/virtualNetworks' } -Tag @{ severity = 'Single point of failure'; category = 'Reliability' } {
|
||||
Rule 'Azure.VirtualNetwork.SingleDNS' -Type 'Microsoft.Network/virtualNetworks' -Tag @{ severity = 'Single point of failure'; category = 'Reliability' } {
|
||||
# If DNS servers are customized, at least two IP addresses should be defined
|
||||
if ($Assert.NullOrEmpty($TargetObject, 'properties.dhcpOptions.dnsServers').Result) {
|
||||
$True;
|
||||
|
@ -33,7 +33,7 @@ Rule 'Azure.VirtualNetwork.SingleDNS' -If { ResourceType 'Microsoft.Network/virt
|
|||
}
|
||||
|
||||
# Synopsis: VNETs should use Azure local DNS servers
|
||||
Rule 'Azure.VirtualNetwork.LocalDNS' -If { ResourceType 'Microsoft.Network/virtualNetworks' } {
|
||||
Rule 'Azure.VirtualNetwork.LocalDNS' -Type 'Microsoft.Network/virtualNetworks' {
|
||||
# If DNS servers are customized, check what range the IPs are in
|
||||
if ($Assert.NullOrEmpty($TargetObject, 'properties.dhcpOptions.dnsServers').Result) {
|
||||
$True;
|
||||
|
@ -66,7 +66,7 @@ Rule 'Azure.VirtualNetwork.PeerState' -If { (HasPeerNetwork) } {
|
|||
#region Network Security Group
|
||||
|
||||
# Synopsis: Network security groups should avoid any inbound rules
|
||||
Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -Type 'Microsoft.Network/networkSecurityGroups' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Recommend 'Avoid rules that apply to all source addresses'
|
||||
|
||||
$rules = $TargetObject.properties.securityRules | Where-Object {
|
||||
|
@ -79,7 +79,7 @@ Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -If { ResourceType 'Microsoft.Ne
|
|||
}
|
||||
|
||||
# Synopsis: Avoid blocking all inbound network traffic
|
||||
Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
|
||||
Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -Type 'Microsoft.Network/networkSecurityGroups' {
|
||||
$denyRules = @(GetOrderedNSGRules | Where-Object {
|
||||
$_.properties.direction -eq 'Inbound' -and
|
||||
$_.properties.access -eq 'Deny' -and
|
||||
|
@ -93,7 +93,7 @@ Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -If { ResourceType 'Microsoft.Netw
|
|||
}
|
||||
|
||||
# Synopsis: Lateral traversal from application servers should be blocked
|
||||
Rule 'Azure.VirtualNetwork.LateralTraversal' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
|
||||
Rule 'Azure.VirtualNetwork.LateralTraversal' -Type 'Microsoft.Network/networkSecurityGroups' {
|
||||
$rules = @($TargetObject.properties.securityRules | Where-Object {
|
||||
$_.properties.direction -eq 'Outbound' -and
|
||||
$_.properties.access -eq 'Deny' -and
|
||||
|
@ -107,7 +107,7 @@ Rule 'Azure.VirtualNetwork.LateralTraversal' -If { ResourceType 'Microsoft.Netwo
|
|||
}
|
||||
|
||||
# Synopsis: Network security groups should be associated to either a subnet or network interface
|
||||
Rule 'Azure.VirtualNetwork.NSGAssociated' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
|
||||
Rule 'Azure.VirtualNetwork.NSGAssociated' -Type 'Microsoft.Network/networkSecurityGroups' {
|
||||
$subnets = ($TargetObject.Properties.subnets | Measure-Object).Count;
|
||||
$interfaces = ($TargetObject.Properties.networkInterfaces | Measure-Object).Count;
|
||||
|
||||
|
@ -120,7 +120,7 @@ Rule 'Azure.VirtualNetwork.NSGAssociated' -If { ResourceType 'Microsoft.Network/
|
|||
#region Application Gateway
|
||||
|
||||
# Synopsis: Application Gateway should use a minimum of two instances
|
||||
Rule 'Azure.VirtualNetwork.AppGwMinInstance' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Important'; category = 'Reliability' } {
|
||||
Rule 'Azure.VirtualNetwork.AppGwMinInstance' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Important'; category = 'Reliability' } {
|
||||
AnyOf {
|
||||
# Applies to v1 and v2 without autoscale
|
||||
$TargetObject.Properties.sku.capacity -ge 2
|
||||
|
@ -131,7 +131,7 @@ Rule 'Azure.VirtualNetwork.AppGwMinInstance' -If { ResourceType 'Microsoft.Netwo
|
|||
}
|
||||
|
||||
# Synopsis: Application Gateway should use a minimum of Medium
|
||||
Rule 'Azure.VirtualNetwork.AppGwMinSku' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Important'; category = 'Performance' } {
|
||||
Rule 'Azure.VirtualNetwork.AppGwMinSku' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Important'; category = 'Performance' } {
|
||||
Within 'Properties.sku.name' 'WAF_Medium', 'Standard_Medium', 'WAF_Large', 'Standard_Large', 'WAF_v2', 'Standard_v2'
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ Rule 'Azure.VirtualNetwork.AppGwUseWAF' -If { (IsAppGwPublic) } -Tag @{ severity
|
|||
}
|
||||
|
||||
# Synopsis: Application Gateway should only accept a minimum of TLS 1.2
|
||||
Rule 'Azure.VirtualNetwork.AppGwSSLPolicy' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Rule 'Azure.VirtualNetwork.AppGwSSLPolicy' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
|
||||
Exists 'Properties.sslPolicy'
|
||||
AnyOf {
|
||||
Within 'Properties.sslPolicy.policyName' 'AppGwSslPolicy20170401S'
|
||||
|
@ -176,7 +176,7 @@ Rule 'Azure.VirtualNetwork.AppGwWAFRules' -If { (IsAppGwWAF) } {
|
|||
#region Network Interface
|
||||
|
||||
# Synopsis: Network interfaces should be attached
|
||||
Rule 'Azure.VirtualNetwork.NICAttached' -If { ResourceType 'Microsoft.Network/networkInterfaces' } {
|
||||
Rule 'Azure.VirtualNetwork.NICAttached' -Type 'Microsoft.Network/networkInterfaces' {
|
||||
Exists 'Properties.virtualMachine.id'
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ Rule 'Azure.VirtualNetwork.NICAttached' -If { ResourceType 'Microsoft.Network/ne
|
|||
#region Load Balancer
|
||||
|
||||
# Synopsis: Use specific network probe
|
||||
Rule 'Azure.VirtualNetwork.LBProbe' -If { ResourceType 'Microsoft.Network/loadBalancers' } {
|
||||
Rule 'Azure.VirtualNetwork.LBProbe' -Type 'Microsoft.Network/loadBalancers' {
|
||||
$probes = $TargetObject.Properties.probes;
|
||||
foreach ($probe in $probes) {
|
||||
if ($probe.properties.port -in 80, 443, 8080) {
|
||||
|
|
|
@ -8,3 +8,4 @@ spec:
|
|||
binding:
|
||||
targetType:
|
||||
- ResourceType
|
||||
- type
|
||||
|
|
|
@ -21,6 +21,7 @@ Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSRule.Rules.Azu
|
|||
$outputPath = Join-Path -Path $rootPath -ChildPath out/tests/PSRule.Rules.Azure.Tests/Cmdlet;
|
||||
Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction Ignore;
|
||||
$Null = New-Item -Path $outputPath -ItemType Directory -Force;
|
||||
$here = (Resolve-Path $PSScriptRoot).Path;
|
||||
|
||||
#region Mocks
|
||||
|
||||
|
@ -71,7 +72,7 @@ function MockContext {
|
|||
|
||||
#region Export-AzRuleData
|
||||
|
||||
Describe 'Export-AzRuleData' -Tag 'Cmdlet' {
|
||||
Describe 'Export-AzRuleData' -Tag 'Cmdlet','Export-AzRuleData' {
|
||||
Context 'With defaults' {
|
||||
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith ${function:MockContext};
|
||||
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
|
||||
|
@ -174,3 +175,92 @@ Describe 'Export-AzRuleData' -Tag 'Cmdlet' {
|
|||
}
|
||||
|
||||
#endregion Export-AzRuleData
|
||||
|
||||
#region Export-AzTemplateRuleData
|
||||
|
||||
Describe 'Export-AzTemplateRuleData' -Tag 'Cmdlet','Export-AzTemplateRuleData' {
|
||||
$templatePath = Join-Path -Path $here -ChildPath 'Resources.Template.json';
|
||||
$parametersPath = Join-Path -Path $here -ChildPath 'Resources.Parameters.json';
|
||||
|
||||
|
||||
Context 'With defaults' {
|
||||
It 'Exports template' {
|
||||
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-defaults.json'
|
||||
$exportParams = @{
|
||||
TemplateFile = $templatePath
|
||||
ParameterFile = $parametersPath
|
||||
OutputPath = $outputFile
|
||||
}
|
||||
$Null = Export-AzTemplateRuleData @exportParams;
|
||||
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
|
||||
$result | Should -Not -BeNullOrEmpty;
|
||||
$result.Length | Should -Be 5;
|
||||
$result[0].name | Should -Be 'vnet-001';
|
||||
$result[0].properties.subnets.Length | Should -Be 3;
|
||||
$result[0].properties.subnets[0].name | Should -Be 'GatewaySubnet';
|
||||
$result[0].properties.subnets[0].properties.addressPrefix | Should -Be '10.1.0.0/27';
|
||||
$result[0].properties.subnets[2].name | Should -Be 'subnet2';
|
||||
$result[0].properties.subnets[2].properties.addressPrefix | Should -Be '10.1.0.64/28';
|
||||
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
|
||||
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/routeTables/route-subnet2$';
|
||||
}
|
||||
}
|
||||
|
||||
Context 'With -Subscription' {
|
||||
Mock -CommandName 'GetSubscription' -ModuleName 'PSRule.Rules.Azure' -MockWith {
|
||||
return [PSCustomObject]@{
|
||||
SubscriptionId = '00000000-0000-0000-0000-000000000000'
|
||||
TenantId = '00000000-0000-0000-0000-000000000000'
|
||||
Name = 'test-sub'
|
||||
}
|
||||
}
|
||||
It 'Exports template' {
|
||||
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-sub.json'
|
||||
$exportParams = @{
|
||||
TemplateFile = $templatePath
|
||||
ParameterFile = $parametersPath
|
||||
OutputPath = $outputFile
|
||||
Subscription = 'test-sub'
|
||||
}
|
||||
$Null = Export-AzTemplateRuleData @exportParams;
|
||||
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
|
||||
$result | Should -Not -BeNullOrEmpty;
|
||||
$result.Length | Should -Be 5;
|
||||
$result[0].properties.subnets.Length | Should -Be 3;
|
||||
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
|
||||
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/routeTables/route-subnet2$';
|
||||
}
|
||||
}
|
||||
|
||||
Context 'With -ResourceGroup' {
|
||||
Mock -CommandName 'GetResourceGroup' -ModuleName 'PSRule.Rules.Azure' -MockWith {
|
||||
return [PSCustomObject]@{
|
||||
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg'
|
||||
ResourceGroupName = 'test-rg'
|
||||
Location = 'region'
|
||||
ManagedBy = 'testuser'
|
||||
Tags = @{
|
||||
test = 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
It 'Exports template' {
|
||||
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-rg.json'
|
||||
$exportParams = @{
|
||||
TemplateFile = $templatePath
|
||||
ParameterFile = $parametersPath
|
||||
OutputPath = $outputFile
|
||||
ResourceGroupName = 'test-rg'
|
||||
}
|
||||
$Null = Export-AzTemplateRuleData @exportParams;
|
||||
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
|
||||
$result | Should -Not -BeNullOrEmpty;
|
||||
$result.Length | Should -Be 5;
|
||||
$result[0].properties.subnets.Length | Should -Be 3;
|
||||
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/test-rg/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
|
||||
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/test-rg/providers/Microsoft\.Network/routeTables/route-subnet2$';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Export-AzTemplateRuleData
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>PSRule.Rules.Azure</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\PSRule.Rules.Azure\PSRule.Rules.Azure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources.Parameters.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources.Template.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"$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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"metadata": {
|
||||
"name": "vnet-hub",
|
||||
"description": "This template creates a hub virtual network."
|
||||
},
|
||||
"parameters": {
|
||||
"vnetName": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The name of the virtual network."
|
||||
}
|
||||
},
|
||||
"addressPrefix": {
|
||||
"type": "array",
|
||||
"minLength": 1,
|
||||
"metadata": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"subnets": {
|
||||
"type": "array",
|
||||
"metadata": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"gatewaySubnet": [
|
||||
{
|
||||
"name": "GatewaySubnet",
|
||||
"properties": {
|
||||
"addressPrefix": "[concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"definedSubnets": {
|
||||
"copy": [
|
||||
{
|
||||
"name": "subnets",
|
||||
"count": "[length(parameters('subnets'))]",
|
||||
"input": {
|
||||
"name": "[parameters('subnets')[copyIndex('subnets')].name]",
|
||||
"properties": {
|
||||
"addressPrefix": "[parameters('subnets')[copyIndex('subnets')].addressPrefix]",
|
||||
"networkSecurityGroup": {
|
||||
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat('nsg-', parameters('subnets')[copyIndex('subnets')].name))]"
|
||||
},
|
||||
"routeTable": {
|
||||
"id": "[resourceId('Microsoft.Network/routeTables', concat('route-', parameters('subnets')[copyIndex('subnets')].name))]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"allSubnets": "[union(variables('gatewaySubnet'), variables('definedSubnets').subnets)]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"comments": "Hub virtual network",
|
||||
"type": "Microsoft.Network/virtualNetworks",
|
||||
"name": "[parameters('vnetName')]",
|
||||
"apiVersion": "2019-04-01",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"routeIndex",
|
||||
"nsgIndex"
|
||||
],
|
||||
"properties": {
|
||||
"addressSpace": {
|
||||
"addressPrefixes": "[parameters('addressPrefix')]"
|
||||
},
|
||||
"subnets": "[variables('allSubnets')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comments": "A subnet Route Table",
|
||||
"type": "Microsoft.Network/routeTables",
|
||||
"name": "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]",
|
||||
"apiVersion": "2019-04-01",
|
||||
"location": "[resourceGroup().location]",
|
||||
"copy": {
|
||||
"name": "routeIndex",
|
||||
"count": "[length(parameters('subnets'))]"
|
||||
},
|
||||
"properties": {
|
||||
"disableBgpRoutePropagation": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"comments": "A subnet Network Security Group",
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"name": "[concat('nsg-', parameters('subnets')[copyIndex()].name)]",
|
||||
"apiVersion": "2019-04-01",
|
||||
"location": "[resourceGroup().location]",
|
||||
"copy": {
|
||||
"name": "nsgIndex",
|
||||
"count": "[length(parameters('subnets'))]"
|
||||
},
|
||||
"properties": {
|
||||
"securityRules": "[parameters('subnets')[copyIndex()].securityRules]"
|
||||
},
|
||||
"dependsOn": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSRule.Rules.Azure.Data.Template;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
|
||||
|
||||
namespace PSRule.Rules.Azure
|
||||
{
|
||||
public sealed class TemplateResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveTemplateTest()
|
||||
{
|
||||
var resources = ProcessTemplate(GetSourcePath("Resources.Template.json"), GetSourcePath("Resources.Parameters.json"));
|
||||
Assert.Equal(5, resources.Length);
|
||||
|
||||
var actual1 = resources[0];
|
||||
Assert.Equal("vnet-001", actual1["name"]);
|
||||
Assert.Equal("10.1.0.0/24", actual1["properties"]["addressSpace"]["addressPrefixes"][0]);
|
||||
Assert.Equal(3, actual1["properties"]["subnets"].Value<JArray>().Count);
|
||||
Assert.Equal("10.1.0.32/28", actual1["properties"]["subnets"][1]["properties"]["addressPrefix"]);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseExpression1()
|
||||
{
|
||||
var expression = "[parameters('vnetName')]";
|
||||
var actual = ExpressionParser.Parse(expression).ToArray();
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // parameters
|
||||
Assert.Equal("parameters", actual[0].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'vnetName'
|
||||
Assert.Equal("vnetName", actual[2].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[3].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseExpression2()
|
||||
{
|
||||
var expression = "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]";
|
||||
var actual = ExpressionParser.Parse(expression).ToArray();
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
|
||||
Assert.Equal("concat", actual[0].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'route-'
|
||||
Assert.Equal("route-", actual[2].Content);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[3].Type); // parameters
|
||||
Assert.Equal("parameters", actual[3].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[4].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[5].Type); // 'subnets'
|
||||
Assert.Equal("subnets", actual[5].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[6].Type);
|
||||
Assert.Equal(ExpressionTokenType.IndexStart, actual[7].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[8].Type); // copyIndex
|
||||
Assert.Equal("copyIndex", actual[8].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[9].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[10].Type); // 'routeIndex'
|
||||
Assert.Equal("routeIndex", actual[10].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[11].Type);
|
||||
Assert.Equal(ExpressionTokenType.IndexEnd, actual[12].Type);
|
||||
Assert.Equal(ExpressionTokenType.Property, actual[13].Type); // .name
|
||||
Assert.Equal("name", actual[13].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[14].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseExpression3()
|
||||
{
|
||||
var expression = "[concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]";
|
||||
var actual = ExpressionParser.Parse(expression).ToArray();
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
|
||||
Assert.Equal("concat", actual[0].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[2].Type); // split
|
||||
Assert.Equal("split", actual[2].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[3].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[4].Type); // parameters
|
||||
Assert.Equal("parameters", actual[4].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[5].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[6].Type); // 'addressPrefix'
|
||||
Assert.Equal("addressPrefix", actual[6].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[7].Type);
|
||||
Assert.Equal(ExpressionTokenType.IndexStart, actual[8].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[9].Type); // 0
|
||||
Assert.Equal("0", actual[9].Content);
|
||||
Assert.Equal(ExpressionTokenType.IndexEnd, actual[10].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[11].Type); // '/'
|
||||
Assert.Equal("/", actual[11].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[12].Type);
|
||||
Assert.Equal(ExpressionTokenType.IndexStart, actual[13].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[14].Type); // 0
|
||||
Assert.Equal("0", actual[14].Content);
|
||||
Assert.Equal(ExpressionTokenType.IndexEnd, actual[15].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[16].Type); // '/27'
|
||||
Assert.Equal("/27", actual[16].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[17].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseExpression4()
|
||||
{
|
||||
var expression = "[concat('route-', parameters('subnets')[0].route[1])]";
|
||||
var actual = ExpressionParser.Parse(expression).ToArray();
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
|
||||
Assert.Equal("concat", actual[0].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'route-'
|
||||
Assert.Equal("route-", actual[2].Content);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[3].Type); // parameters
|
||||
Assert.Equal("parameters", actual[3].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupStart, actual[4].Type);
|
||||
Assert.Equal(ExpressionTokenType.String, actual[5].Type); // 'subnets'
|
||||
Assert.Equal("subnets", actual[5].Content);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[6].Type);
|
||||
Assert.Equal(ExpressionTokenType.IndexStart, actual[7].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[8].Type); // 0
|
||||
Assert.Equal("0", actual[8].Content);
|
||||
Assert.Equal(ExpressionTokenType.IndexEnd, actual[9].Type);
|
||||
Assert.Equal(ExpressionTokenType.Property, actual[10].Type); // .route
|
||||
Assert.Equal("route", actual[10].Content);
|
||||
Assert.Equal(ExpressionTokenType.IndexStart, actual[11].Type);
|
||||
Assert.Equal(ExpressionTokenType.Element, actual[12].Type); // 1
|
||||
Assert.Equal("1", actual[12].Content);
|
||||
Assert.Equal(ExpressionTokenType.IndexEnd, actual[13].Type);
|
||||
Assert.Equal(ExpressionTokenType.GroupEnd, actual[14].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildExpression1()
|
||||
{
|
||||
var expression = "[parameters('vnetName')]";
|
||||
var builder = new ExpressionBuilder();
|
||||
var context = new TemplateContext();
|
||||
context.Parameters["vnetName"] = "vnet1";
|
||||
|
||||
var fn = builder.Build(expression);
|
||||
var actual = fn(context);
|
||||
|
||||
Assert.Equal("vnet1", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildExpression2()
|
||||
{
|
||||
var expression = "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]";
|
||||
var builder = new ExpressionBuilder();
|
||||
var context = new TemplateContext();
|
||||
context.CopyIndex.Push(new TemplateContext.CopyIndexState() { Name = "routeIndex", Index = 0 });
|
||||
context.Parameters["subnets"] = new TestSubnet[] { new TestSubnet("subnet1", new string[] { "routeA", "routeB" }) };
|
||||
|
||||
var fn = builder.Build(expression);
|
||||
var actual = fn(context);
|
||||
|
||||
Assert.Equal("route-subnet1", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildExpression4()
|
||||
{
|
||||
var expression = "[concat('route-', parameters('subnets')[0].route[1])]";
|
||||
var builder = new ExpressionBuilder();
|
||||
var context = new TemplateContext();
|
||||
context.Parameters["subnets"] = new TestSubnet[] { new TestSubnet("subnet1", new string[] { "routeA", "routeB" }) };
|
||||
|
||||
var fn = builder.Build(expression);
|
||||
var actual = fn(context);
|
||||
|
||||
Assert.Equal("route-routeB", actual);
|
||||
}
|
||||
|
||||
private static string GetSourcePath(string fileName)
|
||||
{
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
|
||||
}
|
||||
|
||||
private static JObject[] ProcessTemplate(string templateFile, string parametersFile)
|
||||
{
|
||||
var templateObject = ReadFile<DeploymentTemplate>(templateFile);
|
||||
var parametersObject = ReadFile<DeploymentParameters>(parametersFile);
|
||||
var visitor = new TestTemplateVisitor();
|
||||
visitor.Visit(templateObject, parametersObject);
|
||||
return visitor.TestResources.ToArray();
|
||||
}
|
||||
|
||||
private static T ReadFile<T>(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
return default(T);
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestSubnet
|
||||
{
|
||||
internal TestSubnet(string n, string[] r)
|
||||
{
|
||||
name = n;
|
||||
route = r;
|
||||
}
|
||||
|
||||
public string name { get; private set; }
|
||||
|
||||
public string[] route { get; private set; }
|
||||
}
|
||||
|
||||
internal sealed class TestTemplateVisitor : TemplateVisitor
|
||||
{
|
||||
internal TestTemplateVisitor()
|
||||
: base(null, null)
|
||||
{
|
||||
TestResources = new List<JObject>();
|
||||
}
|
||||
|
||||
public List<JObject> TestResources { get; }
|
||||
|
||||
protected override void ResourceInstance(TemplateContext context, JObject resource)
|
||||
{
|
||||
base.ResourceInstance(context, resource);
|
||||
TestResources.Add(resource);
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче