Compare commits

...

6 Commits

12 changed files with 727 additions and 11 deletions

111
.sample/Deployment.psd1 Normal file
View File

@@ -0,0 +1,111 @@
@{
Templates = @(
@{
TemplateName = "HZD Template"
TemplatePath = "./Configurations/HZD.psd1"
},
@{
TemplateName = "SQL Server Template"
TemplatePath = "./Configurations/Services/Template-SQLServer.psd1"
},
@{
TemplateName = "Umgebungs Template"
TemplatePath = "./Configurations/Umgebungen/Template-Test.psd1"
}
)
AllNodes = @(
@{
NodeName = "JURZMAZSSQL901"
}
)
NonNodeData =@{
Services = @{
SQLServer = @{
Instances = @(
@{
Name = 'MSSQLSERVER'
ServiceAccounts = @{
SQLEngineAccount = @{
ServiceAccountName = "gmsaE2Af901"
AccountType = "Group"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
ManagedPasswordPrincipals = @(
@{
GroupName = "sicE2AfE2ADB901"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
}
)
}
SQLAgentAccount = @{
ServiceAccountName = "gmsaE2Aa901"
AccountType = "Group"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
ManagedPasswordPrincipals = @(
@{
GroupName = "sicE2AfE2ADB901"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
}
)
}
}
AlwaysOn = @{
StartIndex = "B"
AlwaysOnGroups = @(
@{
Name = "TestAG"
FailoverMode ="Automatic"
SeedingMode = "Automatic"
Listener = @{
Provision = $true
IpAddress = "'10.96.x.x/255.255.255.0','10.96.x.x/255.255.255.0'"
Port = 1433
}
}
)
}
ConfigurationOptions= @()
AdditionalScripts = @()
},
@{
Name = 'MSSQLSERVERTEST'
ServiceAccounts = @{
SQLEngineAccount = @{
ServiceAccountName = "gmsaE2Af901"
AccountType = "Group"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
ManagedPasswordPrincipals = @(
@{
GroupName = "sicE2AfE2ADB901"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
}
)
}
SQLAgentAccount = @{
ServiceAccountName = "gmsaE2Aa901"
AccountType = "Group"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
ManagedPasswordPrincipals = @(
@{
GroupName = "sicE2AfE2ADB901"
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
}
)
}
}
ConfigurationOptions= @()
AdditionalScripts = @()
}
)
}
}
}
}

157
.sample/Template.psd1 Normal file
View File

@@ -0,0 +1,157 @@
@{
NonNodeData =@{
Services = @{
SQLServer = @{
General = @{
Version = ""
Release = "2022"
SourcePath = "\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\Binaries"
DestinationPath = "C:\HZD\SQL_Install"
}
Basic = @{
Registry = @(
@{
Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa"
ValueName = "DisableLoopbackCheck"
ValueData = 1
ValueType = "Dword"
},
@{
Ensure = "Present"
Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent"
ValueName = "MsxEncryptChannelOptions"
ValueData = 1
ValueType = "Dword"
}
@{
Ensure = 'Present'
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent'
ValueName = 'ErrorLogFile'
ValueData = 'D:\MSSQL16.MSSQLSERVER\MSSQL\Log\SQLAGENT.OUT'
ValueType = 'String'
},
@{
Ensure = 'Present'
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent'
ValueName = 'WorkingDirectory'
ValueData = 'D:\MSSQL16.MSSQLSERVER\MSSQL\JOBS'
ValueType = 'String'
}
)
WindowsFeatures = @(
@{
Name = "RSAT-AD-PowerShell"
}
)
Modules = @(
@{
Name = "SQLServer"
AllowClobber = $true
}
)
}
Instances = @(
@{
Name = '*'
Features = 'SQLENGINE,FULLTEXT'
SQLSysAdminAccounts = 'sicDBServer_ServerAdmins'
Directories = @{
InstallSharedDir = 'D:\Microsoft SQL Server'
InstallSharedWOWDir = 'D:\Microsoft SQL Server (x86)'
InstanceDir = 'D:\Microsoft SQL Server'
InstallSQLDataDir = 'D:'
SQLSysAdminAccounts = 'sicDBServer_ServerAdmins'
SQLBackupDir = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\Backups'
SQLUserDBDir = ''
SQLUserDBLogDir = ''
SQLTempDBDir = ''
SQLTempDBLogDir = ''
}
Drives = @{
}
MountPoints = @{
}
Memory = @{
UseDynamic = $true
minValue = 1024
maxValue = 8192
}
ServiceAccounts = @{
}
AlwaysOn = @{
}
ConfigurationOptions= @(
@{
OptionName = 'show advanced options'
OptionValue = '1'
},
@{
OptionName = 'cost threshold for parallelism'
OptionValue = '50'
},
@{
OptionName = 'blocked process threshold (s)'
OptionValue = '30'
},
@{
OptionName = 'backup compression default'
OptionValue = '1'
}
)
AdditionalScripts = @(
@{
ScriptName = 'AdminDB'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\AdminDB'
DependsOn = '[SqlSetup]Instance-MSSQLSERVER'
},
@{
ScriptName = 'RenameSA'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\RenameSA'
DependsOn = '[SqlScript]AdminDB'
},
@{
ScriptName = 'Maintenance_Solution'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\Maintenance_Solutions'
DependsOn = '[SqlScript]RenameSA'
},
@{
ScriptName = 'WhoIsActive'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\WhoIsActive_Monitoring'
DependsOn = '[SqlScript]Maintenance_Solution'
},
@{
ScriptName = 'Alerts'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\Alerts_Email'
DependsOn = '[SqlScript]WhoIsActive'
},
@{
ScriptName = 'TempDB'
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\TempDB_Size'
DependsOn = '[SqlScript]Alerts'
}
)
}
)
}
}
}
}

View File

@@ -0,0 +1,132 @@
$script:ModuleRoot = Split-Path -Parent $PSScriptRoot
$script:SampleRoot = Join-Path -Path $script:ModuleRoot -ChildPath ".sample"
Import-Module (Join-Path -Path $script:ModuleRoot -ChildPath "Merge-DSCConfigurationData.psd1") -Force
Describe "Merge-DSCConfigurationData" {
It "merges wildcard instance templates into all deployment instances" {
$Template = Import-PowerShellDataFile (Join-Path -Path $script:SampleRoot -ChildPath "Template.psd1")
$Deployment = Import-PowerShellDataFile (Join-Path -Path $script:SampleRoot -ChildPath "Deployment.psd1")
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
$Instances = @($Result.NonNodeData.Services.SQLServer.Instances)
$Instances.Count | Should Be 2
($Instances.Name -contains "*") | Should Be $false
foreach($Name in @("MSSQLSERVER", "MSSQLSERVERTEST")){
$Instance = $Instances | Where-Object { $_.Name -eq $Name }
$Instance | Should Not BeNullOrEmpty
$Instance.Features | Should Be "SQLENGINE,FULLTEXT"
$Instance.ConfigurationOptions.Count | Should Be 4
$Instance.AdditionalScripts.Count | Should Be 6
$Instance.Memory | Should Not BeNullOrEmpty
$Instance.Directories | Should Not BeNullOrEmpty
}
}
It "does not merge an exact instance template into another instance name" {
$Template = @{
NonNodeData = @{
Services = @{
SQLServer = @{
Instances = @(
@{
Name = "MSSQLSERVER"
Marker = "exact-only"
}
)
}
}
}
}
$Deployment = @{
NonNodeData = @{
Services = @{
SQLServer = @{
Instances = @(
@{ Name = "MSSQLSERVER" },
@{ Name = "MSSQLSERVERTEST" }
)
}
}
}
}
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
$Instances = @($Result.NonNodeData.Services.SQLServer.Instances)
($Instances | Where-Object { $_.Name -eq "MSSQLSERVER" }).Marker | Should Be "exact-only"
($Instances | Where-Object { $_.Name -eq "MSSQLSERVERTEST" }).Marker | Should BeNullOrEmpty
}
It "uses the name-key fallback for unknown arrays" {
$Template = @{
Databases = @(
@{
DatabaseName = "AppDb"
RecoveryModel = "Full"
}
)
}
$Deployment = @{
Databases = @(
@{
DatabaseName = "AppDb"
Owner = "dbo"
},
@{
DatabaseName = "LogDb"
Owner = "dbo"
}
)
}
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
$Databases = @($Result.Databases)
($Databases | Where-Object { $_.DatabaseName -eq "AppDb" }).RecoveryModel | Should Be "Full"
($Databases | Where-Object { $_.DatabaseName -eq "LogDb" }).RecoveryModel | Should BeNullOrEmpty
}
It "uses Key and ValueName together for registry array merges" {
$Template = @{
Basic = @{
Registry = @(
@{
Key = "K1"
ValueName = "Enabled"
Default = "Template-K1"
},
@{
Key = "K2"
ValueName = "Enabled"
Default = "Template-K2"
}
)
}
}
$Deployment = @{
Basic = @{
Registry = @(
@{
Key = "K1"
ValueName = "Enabled"
ValueData = "Deployment-K1"
}
)
}
}
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
$Registry = @($Result.Basic.Registry)
($Registry | Where-Object { $_.Key -eq "K1" }).Default | Should Be "Template-K1"
($Registry | Where-Object { $_.Key -eq "K1" }).ValueData | Should Be "Deployment-K1"
($Registry | Where-Object { $_.Key -eq "K2" }).Default | Should Be "Template-K2"
}
}

View File

@@ -1,6 +1,6 @@
@{
RootModule = "Merge-DSCConfigurationData.psm1"
ModuleVersion = "1.0.1"
ModuleVersion = "1.1.0"
GUID = "c1c7e70d-9049-4eaa-a3c9-44a424c35ef5"
Author = "Torsten Brendgen"
Copyright = "(c) Torsten Brendgen. All rights reserved."
@@ -9,7 +9,8 @@
FunctionsToExport = @(
"Merge-DSCConfigurationData",
"Export-PowerShellDataFile",
"New-DSCConfigurationDataDeployment"
"New-DSCConfigurationDataDeployment",
"Test-DSCConfigurationDataDeployment"
)
CmdletsToExport = @()
VariablesToExport = @()

View File

@@ -11,5 +11,6 @@ foreach($File in @($Private + $Public)){
Export-ModuleMember -Function @(
"Merge-DSCConfigurationData",
"Export-PowerShellDataFile",
"New-DSCConfigurationDataDeployment"
"New-DSCConfigurationDataDeployment",
"Test-DSCConfigurationDataDeployment"
)

View 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")
}
}

View 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
}

View File

@@ -0,0 +1,14 @@
function Test-ConfigurationDataNodeSealed {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$Node
)
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $Node -Key "Sealed")){
return $false
}
return [System.Management.Automation.LanguagePrimitives]::ConvertTo($Node["Sealed"], [bool])
}

View File

@@ -20,7 +20,11 @@ function Merge-DSCConfigurationData {
[Parameter(Mandatory=$false, ParameterSetName="Path")]
[switch]
$PassThru
$PassThru,
[Parameter(Mandatory=$false, ParameterSetName="Data")]
[string]
$MergePath = '$'
)
begin {
@@ -37,6 +41,10 @@ function Merge-DSCConfigurationData {
$Output = $Deployment
}
if((Test-ConfigurationDataNodeSealed -Node $Template) -and $Deployment.Count -gt 0){
throw "Configuration data path [$MergePath] is sealed and cannot be modified."
}
foreach($Property in $Template.GetEnumerator()){
if($Property.Name -eq "Metadata"){
Write-Verbose "Skipping metadata block during configuration data merge"
@@ -47,7 +55,7 @@ function Merge-DSCConfigurationData {
Write-Verbose "Key [$($Property.Name)] is a Dictionary"
if(Test-ConfigurationDataDictionaryKey -Dictionary $Deployment -Key $Property.Name){
Write-Verbose "Key [$($Property.Name)] is present in Deployment Data"
$Output[$Property.Name] = Merge-DSCConfigurationData -Template $Template[$Property.Name] -Deployment $Deployment[$Property.Name] -Output $Output[$Property.Name]
$Output[$Property.Name] = Merge-DSCConfigurationData -Template $Template[$Property.Name] -Deployment $Deployment[$Property.Name] -Output $Output[$Property.Name] -MergePath "$MergePath.$($Property.Name)"
}else{
Write-Verbose "Key [$($Property.Name)] is not present in Deployment Data"
$Output.Add($Property.Name,(Copy-ConfigurationDataValue -Value $Template[$Property.Name]))
@@ -89,7 +97,7 @@ function Merge-DSCConfigurationData {
if($MatchingDeploymentItems.Count -gt 0){
foreach($DeploymentItem in $MatchingDeploymentItems){
$OutputItem = @($Output[$Property.Name] | Where-Object { Test-ConfigurationDataItemKeyMatch -Left $_ -Right $DeploymentItem -KeyNames $SearchKeyNames })[0]
Merge-DSCConfigurationData -Template $TemplateItem.Value -Deployment $DeploymentItem -Output $OutputItem | Out-Null
Merge-DSCConfigurationData -Template $TemplateItem.Value -Deployment $DeploymentItem -Output $OutputItem -MergePath "$MergePath.$($Property.Name)[$(Format-ConfigurationDataMergeKey -Item $DeploymentItem -KeyNames $SearchKeyNames)]" | Out-Null
}
}else{
if($TemplateItem.IsWildcard){

View File

@@ -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]@{

View 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)
}

View File

@@ -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,112 @@ 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
}
It 'prevents overriding a sealed parameter definition' {
$Template = @{
Parameters = @{
DomainNetBIOS = @{
Type = 'string'
Value = 'CONTOSO'
Sealed = $true
}
}
}
$Deployment = @{
Parameters = @{
DomainNetBIOS = @{
Value = 'OTHER'
}
}
}
{ Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw
}
It 'prevents adding values below a sealed resource tree' {
$Template = @{
Resources = @{
NonNodeData = @{
Services = @{
SharePoint = @{
Farm = @{
ManagedAccounts = @{
Sealed = $true
FarmAccount = "[parameters('FarmCredential')]"
}
}
}
}
}
}
}
$Deployment = @{
Resources = @{
NonNodeData = @{
Services = @{
SharePoint = @{
Farm = @{
ManagedAccounts = @{
SearchAccount = "[parameters('SearchCredential')]"
}
}
}
}
}
}
}
{ Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw
}
}