Add Test-DSCConfigurationDataDeployment function and related enhancements
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
FunctionsToExport = @(
|
||||
"Merge-DSCConfigurationData",
|
||||
"Export-PowerShellDataFile",
|
||||
"New-DSCConfigurationDataDeployment"
|
||||
"New-DSCConfigurationDataDeployment",
|
||||
"Test-DSCConfigurationDataDeployment"
|
||||
)
|
||||
CmdletsToExport = @()
|
||||
VariablesToExport = @()
|
||||
|
||||
@@ -11,5 +11,6 @@ foreach($File in @($Private + $Public)){
|
||||
Export-ModuleMember -Function @(
|
||||
"Merge-DSCConfigurationData",
|
||||
"Export-PowerShellDataFile",
|
||||
"New-DSCConfigurationDataDeployment"
|
||||
"New-DSCConfigurationDataDeployment",
|
||||
"Test-DSCConfigurationDataDeployment"
|
||||
)
|
||||
|
||||
34
Private/Get-ConfigurationDataSourceFileInfo.ps1
Normal file
34
Private/Get-ConfigurationDataSourceFileInfo.ps1
Normal file
@@ -0,0 +1,34 @@
|
||||
function Get-ConfigurationDataSourceFileInfo {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$BasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$UseRelativePath
|
||||
)
|
||||
|
||||
$ResolvedPath = Resolve-ConfigurationDataTemplatePath -Path $Path -BasePath $BasePath
|
||||
$Item = Get-Item -LiteralPath $ResolvedPath
|
||||
$Hash = Get-FileHash -LiteralPath $ResolvedPath -Algorithm SHA256
|
||||
|
||||
$DisplayPath = if($UseRelativePath){
|
||||
ConvertTo-ConfigurationDataRelativePath -Path $ResolvedPath -BasePath $BasePath
|
||||
}else{
|
||||
$ResolvedPath
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
Path = $DisplayPath
|
||||
Hash = $Hash.Hash
|
||||
Algorithm = $Hash.Algorithm
|
||||
Size = $Item.Length
|
||||
LastWriteTimeUtc = $Item.LastWriteTimeUtc.ToString("o")
|
||||
}
|
||||
}
|
||||
19
Private/Resolve-ConfigurationDataSourcePath.ps1
Normal file
19
Private/Resolve-ConfigurationDataSourcePath.ps1
Normal file
@@ -0,0 +1,19 @@
|
||||
function Resolve-ConfigurationDataSourcePath {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$DeploymentPath
|
||||
)
|
||||
|
||||
$BasePath = Split-Path -Path (Repair-ConfigurationDataPathEncoding -Path $DeploymentPath) -Parent
|
||||
if([string]::IsNullOrWhiteSpace($BasePath)){
|
||||
$BasePath = (Get-Location).Path
|
||||
}
|
||||
|
||||
return Resolve-ConfigurationDataTemplatePath -Path $Path -BasePath $BasePath
|
||||
}
|
||||
@@ -91,11 +91,7 @@ function New-DSCConfigurationDataDeployment {
|
||||
})
|
||||
|
||||
$SourceFiles = @($MergeResult.Files | ForEach-Object {
|
||||
if($UseRelativePaths){
|
||||
ConvertTo-ConfigurationDataRelativePath -Path $_ -BasePath $OutputDirectory
|
||||
}else{
|
||||
$_
|
||||
}
|
||||
Get-ConfigurationDataSourceFileInfo -Path $_ -BasePath $OutputDirectory -UseRelativePath:$UseRelativePaths
|
||||
})
|
||||
|
||||
$ConfigurationData["Metadata"] = [ordered]@{
|
||||
|
||||
125
Public/Test-DSCConfigurationDataDeployment.ps1
Normal file
125
Public/Test-DSCConfigurationDataDeployment.ps1
Normal file
@@ -0,0 +1,125 @@
|
||||
function Test-DSCConfigurationDataDeployment {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$PassThru
|
||||
)
|
||||
|
||||
$ResolvedPath = Repair-ConfigurationDataPathEncoding -Path $Path
|
||||
$Issues = @()
|
||||
|
||||
if(-not (Test-Path -LiteralPath $ResolvedPath -PathType Leaf)){
|
||||
$Issues += [PSCustomObject]@{
|
||||
Severity = "Error"
|
||||
Code = "DeploymentNotFound"
|
||||
Path = $ResolvedPath
|
||||
Message = "Deployment file was not found."
|
||||
}
|
||||
}else{
|
||||
try {
|
||||
$DeploymentData = Import-PowerShellDataFile -LiteralPath $ResolvedPath
|
||||
}
|
||||
catch {
|
||||
$Issues += [PSCustomObject]@{
|
||||
Severity = "Error"
|
||||
Code = "DeploymentImportFailed"
|
||||
Path = $ResolvedPath
|
||||
Message = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($null -ne $DeploymentData){
|
||||
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Metadata")){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "MetadataMissing"; Path = $ResolvedPath; Message = "Metadata block is missing." }
|
||||
}elseif($DeploymentData.Metadata.TemplateType -ne "Deployment"){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "InvalidTemplateType"; Path = $ResolvedPath; Message = "Metadata.TemplateType must be [Deployment]." }
|
||||
}
|
||||
|
||||
foreach($Key in @("Parameters", "Variables", "Resources")){
|
||||
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key $Key)){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "$Key`Missing"; Path = $ResolvedPath; Message = "Required block [$Key] is missing." }
|
||||
}
|
||||
}
|
||||
|
||||
if((Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Resources") -and
|
||||
(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Resources -Key "AllNodes"))){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "AllNodesMissing"; Path = $ResolvedPath; Message = "Resources.AllNodes is missing." }
|
||||
}
|
||||
|
||||
if((Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Resources") -and
|
||||
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Resources -Key "AllNodes")){
|
||||
$NodeNames = @{}
|
||||
foreach($Node in @($DeploymentData.Resources.AllNodes)){
|
||||
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $Node -Key "NodeName") -or [string]::IsNullOrWhiteSpace([string]$Node.NodeName)){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "NodeNameMissing"; Path = $ResolvedPath; Message = "An AllNodes item has no NodeName." }
|
||||
continue
|
||||
}
|
||||
|
||||
$NodeName = [string]$Node.NodeName
|
||||
if($NodeNames.ContainsKey($NodeName)){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "DuplicateNodeName"; Path = $ResolvedPath; Message = "Duplicate NodeName [$NodeName]." }
|
||||
}else{
|
||||
$NodeNames[$NodeName] = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($DeploymentData.Metadata -is [System.Collections.IDictionary] -and
|
||||
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Metadata -Key "Sources") -and
|
||||
$DeploymentData.Metadata.Sources -is [System.Collections.IDictionary] -and
|
||||
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Metadata.Sources -Key "Files")){
|
||||
foreach($Source in @($DeploymentData.Metadata.Sources.Files)){
|
||||
$SourcePath = if($Source -is [System.Collections.IDictionary]){ [string]$Source.Path } else { [string]$Source }
|
||||
if([string]::IsNullOrWhiteSpace($SourcePath)){
|
||||
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourcePathMissing"; Path = $ResolvedPath; Message = "A source file entry has no path." }
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
$ActualPath = Resolve-ConfigurationDataSourcePath -Path $SourcePath -DeploymentPath $ResolvedPath
|
||||
}
|
||||
catch {
|
||||
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourceMissing"; Path = $SourcePath; Message = $_.Exception.Message }
|
||||
continue
|
||||
}
|
||||
|
||||
if($Source -is [System.Collections.IDictionary] -and (Test-ConfigurationDataDictionaryKey -Dictionary $Source -Key "Hash")){
|
||||
$Algorithm = [string](Get-ConfigurationDataDictionaryValue -Dictionary $Source -Key "Algorithm" -DefaultValue "SHA256")
|
||||
$ActualHash = Get-FileHash -LiteralPath $ActualPath -Algorithm $Algorithm
|
||||
if($ActualHash.Hash -ne [string]$Source.Hash){
|
||||
$Issues += [PSCustomObject]@{
|
||||
Severity = "Warning"
|
||||
Code = "SourceChanged"
|
||||
Path = $SourcePath
|
||||
Message = "Source file hash has changed."
|
||||
ExpectedHash = [string]$Source.Hash
|
||||
ActualHash = $ActualHash.Hash
|
||||
Algorithm = $Algorithm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourceMetadataMissing"; Path = $ResolvedPath; Message = "Metadata.Sources.Files is missing." }
|
||||
}
|
||||
}
|
||||
|
||||
$Result = [PSCustomObject]@{
|
||||
Path = $ResolvedPath
|
||||
IsValid = @($Issues | Where-Object { $_.Severity -eq "Error" }).Count -eq 0
|
||||
IsStale = @($Issues | Where-Object { $_.Code -in @("SourceChanged", "SourceMissing") }).Count -gt 0
|
||||
Issues = $Issues
|
||||
}
|
||||
|
||||
if($PassThru){
|
||||
return $Result
|
||||
}
|
||||
|
||||
return $Result.IsValid -and (-not $Result.IsStale)
|
||||
}
|
||||
@@ -212,6 +212,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
Required = $true
|
||||
}
|
||||
}
|
||||
Variables = @{
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
@@ -250,6 +252,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
$DeploymentPath | Should Exist
|
||||
$Result.ConfigurationData.Metadata.TemplateType | Should Be 'Deployment'
|
||||
$Result.ConfigurationData.Metadata.Sources.Files.Count | Should Be 2
|
||||
$Result.ConfigurationData.Metadata.Sources.Files[0].Hash.Length | Should Be 64
|
||||
$Result.ConfigurationData.Metadata.Sources.Files[0].Algorithm | Should Be 'SHA256'
|
||||
$Result.ConfigurationData.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||
$Result.ConfigurationData.Resources.AllNodes[0].NodeName | Should Be 'Node01'
|
||||
$Result.ConfigurationData.Resources.AllNodes[0].RunCentralAdministration | Should Be $true
|
||||
@@ -257,6 +261,10 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
$Imported = Import-PowerShellDataFile -Path $DeploymentPath
|
||||
$Imported.Metadata.Name | Should Be 'Contoso-Test'
|
||||
$Imported.Metadata.Sources.Templates.Count | Should Be 1
|
||||
|
||||
$Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru
|
||||
$Validation.IsValid | Should Be $true
|
||||
$Validation.IsStale | Should Be $false
|
||||
}
|
||||
|
||||
It 'exports materialized deployment data as JSON' {
|
||||
@@ -271,6 +279,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
Value = 'contoso.local'
|
||||
}
|
||||
}
|
||||
Variables = @{
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
@@ -288,4 +298,53 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
$Imported.Metadata.TemplateType | Should Be 'Deployment'
|
||||
$Imported.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||
}
|
||||
|
||||
It 'detects changed deployment source files by hash' {
|
||||
$DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1'
|
||||
$DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployment_123.psd1'
|
||||
|
||||
@'
|
||||
@{
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Type = 'string'
|
||||
Value = 'contoso.local'
|
||||
}
|
||||
}
|
||||
Variables = @{
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
New-DSCConfigurationDataDeployment `
|
||||
-Path $DeploymentPath `
|
||||
-Name 'Contoso-Test' `
|
||||
-DeploymentId '123' `
|
||||
-SourceTemplatePath $DefaultPath `
|
||||
-AllNodes @(
|
||||
@{
|
||||
NodeName = 'Node01'
|
||||
}
|
||||
) `
|
||||
-Export `
|
||||
-Force
|
||||
|
||||
@'
|
||||
@{
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Type = 'string'
|
||||
Value = 'changed.local'
|
||||
}
|
||||
}
|
||||
Variables = @{
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
$Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru
|
||||
$Validation.IsValid | Should Be $true
|
||||
$Validation.IsStale | Should Be $true
|
||||
@($Validation.Issues | Where-Object { $_.Code -eq 'SourceChanged' }).Count | Should Be 1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user