Update module version to 1.0.1 and add new functions for exporting and deploying DSC configuration data
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
@{
|
||||
@{
|
||||
RootModule = "Merge-DSCConfigurationData.psm1"
|
||||
ModuleVersion = "1.0.0"
|
||||
ModuleVersion = "1.0.1"
|
||||
GUID = "c1c7e70d-9049-4eaa-a3c9-44a424c35ef5"
|
||||
Author = "Torsten Brendgen"
|
||||
Copyright = "(c) Torsten Brendgen. All rights reserved."
|
||||
Description = "Merges DSC configuration data from templates and deployment data."
|
||||
PowerShellVersion = "5.1"
|
||||
FunctionsToExport = @(
|
||||
"Merge-DSCConfigurationData"
|
||||
"Merge-DSCConfigurationData",
|
||||
"Export-PowerShellDataFile",
|
||||
"New-DSCConfigurationDataDeployment"
|
||||
)
|
||||
CmdletsToExport = @()
|
||||
VariablesToExport = @()
|
||||
|
||||
@@ -9,5 +9,7 @@ foreach($File in @($Private + $Public)){
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
"Merge-DSCConfigurationData"
|
||||
"Merge-DSCConfigurationData",
|
||||
"Export-PowerShellDataFile",
|
||||
"New-DSCConfigurationDataDeployment"
|
||||
)
|
||||
|
||||
29
Private/ConvertTo-ConfigurationDataRelativePath.ps1
Normal file
29
Private/ConvertTo-ConfigurationDataRelativePath.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
function ConvertTo-ConfigurationDataRelativePath {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$BasePath
|
||||
)
|
||||
|
||||
$ResolvedPath = Repair-ConfigurationDataPathEncoding -Path $Path
|
||||
$ResolvedBasePath = Repair-ConfigurationDataPathEncoding -Path $BasePath
|
||||
|
||||
try {
|
||||
$PathUri = [System.Uri]::new((Resolve-Path -LiteralPath $ResolvedPath).ProviderPath)
|
||||
$BaseUriPath = [System.IO.Path]::GetFullPath($ResolvedBasePath)
|
||||
if(-not $BaseUriPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)){
|
||||
$BaseUriPath += [System.IO.Path]::DirectorySeparatorChar
|
||||
}
|
||||
|
||||
$BaseUri = [System.Uri]::new($BaseUriPath)
|
||||
return [System.Uri]::UnescapeDataString($BaseUri.MakeRelativeUri($PathUri).ToString()).Replace('/', [System.IO.Path]::DirectorySeparatorChar)
|
||||
}
|
||||
catch {
|
||||
return $ResolvedPath
|
||||
}
|
||||
}
|
||||
64
Private/ConvertTo-PowerShellDataFileText.ps1
Normal file
64
Private/ConvertTo-PowerShellDataFileText.ps1
Normal file
@@ -0,0 +1,64 @@
|
||||
function ConvertTo-PowerShellDataFileText {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[AllowNull()]
|
||||
$InputObject,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]
|
||||
$Indent = 0
|
||||
)
|
||||
|
||||
$IndentText = " " * $Indent
|
||||
$ChildIndent = $Indent + 4
|
||||
$ChildIndentText = " " * $ChildIndent
|
||||
|
||||
if($null -eq $InputObject){
|
||||
return '$null'
|
||||
}
|
||||
|
||||
if($InputObject -is [bool]){
|
||||
if($InputObject){
|
||||
return '$true'
|
||||
}
|
||||
|
||||
return '$false'
|
||||
}
|
||||
|
||||
if($InputObject -is [int] -or $InputObject -is [long] -or $InputObject -is [decimal] -or $InputObject -is [double]){
|
||||
return ([string]$InputObject)
|
||||
}
|
||||
|
||||
if($InputObject -is [string]){
|
||||
return "'$($InputObject.Replace("'", "''"))'"
|
||||
}
|
||||
|
||||
if($InputObject -is [System.Collections.IDictionary]){
|
||||
$Lines = @("@{")
|
||||
foreach($Key in $InputObject.Keys){
|
||||
$KeyText = "'$(([string]$Key).Replace("'", "''"))'"
|
||||
$ValueText = ConvertTo-PowerShellDataFileText -InputObject $InputObject[$Key] -Indent $ChildIndent
|
||||
$Lines += "$ChildIndentText$KeyText = $ValueText"
|
||||
}
|
||||
$Lines += "$IndentText}"
|
||||
return ($Lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
if($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]){
|
||||
$Items = @($InputObject)
|
||||
if($Items.Count -eq 0){
|
||||
return '@()'
|
||||
}
|
||||
|
||||
$Lines = @("@(")
|
||||
foreach($Item in $Items){
|
||||
$ValueText = ConvertTo-PowerShellDataFileText -InputObject $Item -Indent $ChildIndent
|
||||
$Lines += "$ChildIndentText$ValueText"
|
||||
}
|
||||
$Lines += "$IndentText)"
|
||||
return ($Lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
return "'$(([string]$InputObject).Replace("'", "''"))'"
|
||||
}
|
||||
21
Private/Repair-ConfigurationDataPathEncoding.ps1
Normal file
21
Private/Repair-ConfigurationDataPathEncoding.ps1
Normal file
@@ -0,0 +1,21 @@
|
||||
function Repair-ConfigurationDataPathEncoding {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[AllowEmptyString()]
|
||||
[string]
|
||||
$Path
|
||||
)
|
||||
|
||||
if($Path -notmatch "[ÃÂ]"){
|
||||
return $Path
|
||||
}
|
||||
|
||||
try {
|
||||
$Bytes = [System.Text.Encoding]::GetEncoding(1252).GetBytes($Path)
|
||||
return [System.Text.Encoding]::UTF8.GetString($Bytes)
|
||||
}
|
||||
catch {
|
||||
return $Path
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,31 @@ function Resolve-ConfigurationDataTemplatePath {
|
||||
$BasePath = (Get-Location).Path
|
||||
)
|
||||
|
||||
$CandidatePath = $Path
|
||||
if(-not [System.IO.Path]::IsPathRooted($CandidatePath)){
|
||||
$CandidatePath = Join-Path -Path $BasePath -ChildPath $CandidatePath
|
||||
$CandidatePaths = @()
|
||||
foreach($CurrentPath in @($Path, (Repair-ConfigurationDataPathEncoding -Path $Path))){
|
||||
if([string]::IsNullOrWhiteSpace($CurrentPath)){
|
||||
continue
|
||||
}
|
||||
|
||||
$CandidatePath = $CurrentPath
|
||||
$CandidateBasePath = Repair-ConfigurationDataPathEncoding -Path $BasePath
|
||||
if(-not [System.IO.Path]::IsPathRooted($CandidatePath)){
|
||||
$CandidatePath = Join-Path -Path $CandidateBasePath -ChildPath $CandidatePath
|
||||
}
|
||||
|
||||
if($CandidatePaths -notcontains $CandidatePath){
|
||||
$CandidatePaths += $CandidatePath
|
||||
}
|
||||
}
|
||||
|
||||
$ResolvedPath = $null
|
||||
foreach($CandidatePath in $CandidatePaths){
|
||||
$ResolvedPath = Resolve-Path -LiteralPath $CandidatePath -ErrorAction SilentlyContinue
|
||||
if($null -ne $ResolvedPath){
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$ResolvedPath = Resolve-Path -Path $CandidatePath -ErrorAction SilentlyContinue
|
||||
if($null -eq $ResolvedPath){
|
||||
throw "Configuration data template [$Path] was not found. Base path [$BasePath]."
|
||||
}
|
||||
|
||||
32
Public/Export-PowerShellDataFile.ps1
Normal file
32
Public/Export-PowerShellDataFile.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
function Export-PowerShellDataFile {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
|
||||
[System.Collections.IDictionary]
|
||||
$InputObject,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$Force
|
||||
)
|
||||
|
||||
process {
|
||||
$ResolvedPath = Repair-ConfigurationDataPathEncoding -Path $Path
|
||||
|
||||
if((Test-Path -LiteralPath $ResolvedPath -PathType Leaf) -and (-not $Force)){
|
||||
throw "File [$ResolvedPath] already exists. Use -Force to overwrite it."
|
||||
}
|
||||
|
||||
$Parent = Split-Path -Path $ResolvedPath -Parent
|
||||
if(-not [string]::IsNullOrWhiteSpace($Parent) -and -not (Test-Path -LiteralPath $Parent)){
|
||||
New-Item -Path $Parent -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$Text = ConvertTo-PowerShellDataFileText -InputObject $InputObject
|
||||
Set-Content -LiteralPath $ResolvedPath -Value $Text -Encoding UTF8
|
||||
}
|
||||
}
|
||||
143
Public/New-DSCConfigurationDataDeployment.ps1
Normal file
143
Public/New-DSCConfigurationDataDeployment.ps1
Normal file
@@ -0,0 +1,143 @@
|
||||
function New-DSCConfigurationDataDeployment {
|
||||
[CmdletBinding(SupportsShouldProcess=$true)]
|
||||
Param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$Name,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]
|
||||
$DeploymentId,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("SourceTemplates")]
|
||||
[string[]]
|
||||
$SourceTemplatePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[hashtable[]]
|
||||
$AllNodes = @(),
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[System.Collections.IDictionary]
|
||||
$DeploymentData = @{},
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$UseRelativePaths,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$Export,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("PowerShellDataFile", "Json")]
|
||||
[string]
|
||||
$Format = "PowerShellDataFile",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$Force,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]
|
||||
$PassThru
|
||||
)
|
||||
|
||||
if($Export -and [string]::IsNullOrWhiteSpace($Path)){
|
||||
throw "Parameter [Path] is required when [Export] is used."
|
||||
}
|
||||
|
||||
$ResolvedOutputPath = $null
|
||||
if(-not [string]::IsNullOrWhiteSpace($Path)){
|
||||
$ResolvedOutputPath = Repair-ConfigurationDataPathEncoding -Path $Path
|
||||
}
|
||||
|
||||
$OutputDirectory = if([string]::IsNullOrWhiteSpace($ResolvedOutputPath)){
|
||||
(Get-Location).Path
|
||||
}else{
|
||||
Split-Path -Path $ResolvedOutputPath -Parent
|
||||
}
|
||||
|
||||
if([string]::IsNullOrWhiteSpace($OutputDirectory)){
|
||||
$OutputDirectory = (Get-Location).Path
|
||||
}
|
||||
|
||||
$MergeResult = Merge-DSCConfigurationData -Path $SourceTemplatePath -PassThru
|
||||
$MergedTemplateData = $MergeResult.ConfigurationData
|
||||
|
||||
$DeploymentOverride = Copy-ConfigurationDataValue -Value $DeploymentData
|
||||
if($AllNodes.Count -gt 0){
|
||||
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentOverride -Key "Resources")){
|
||||
$DeploymentOverride["Resources"] = @{}
|
||||
}
|
||||
|
||||
$DeploymentOverride["Resources"]["AllNodes"] = $AllNodes
|
||||
}
|
||||
|
||||
$ConfigurationData = Merge-DSCConfigurationData -Template $MergedTemplateData -Deployment $DeploymentOverride
|
||||
|
||||
$TemplatePaths = @($SourceTemplatePath | ForEach-Object {
|
||||
$Resolved = Resolve-ConfigurationDataTemplatePath -Path $_
|
||||
if($UseRelativePaths){
|
||||
ConvertTo-ConfigurationDataRelativePath -Path $Resolved -BasePath $OutputDirectory
|
||||
}else{
|
||||
$Resolved
|
||||
}
|
||||
})
|
||||
|
||||
$SourceFiles = @($MergeResult.Files | ForEach-Object {
|
||||
if($UseRelativePaths){
|
||||
ConvertTo-ConfigurationDataRelativePath -Path $_ -BasePath $OutputDirectory
|
||||
}else{
|
||||
$_
|
||||
}
|
||||
})
|
||||
|
||||
$ConfigurationData["Metadata"] = [ordered]@{
|
||||
TemplateType = "Deployment"
|
||||
Name = $Name
|
||||
DeploymentId = $DeploymentId
|
||||
GeneratedOn = (Get-Date).ToString("s")
|
||||
Sources = [ordered]@{
|
||||
Templates = $TemplatePaths
|
||||
Files = $SourceFiles
|
||||
}
|
||||
}
|
||||
|
||||
if(-not $Export){
|
||||
return $ConfigurationData
|
||||
}
|
||||
|
||||
if($PSCmdlet.ShouldProcess($ResolvedOutputPath, "Export DSC configuration data deployment as [$Format]")){
|
||||
switch($Format){
|
||||
"PowerShellDataFile" {
|
||||
Export-PowerShellDataFile -InputObject $ConfigurationData -Path $ResolvedOutputPath -Force:$Force
|
||||
}
|
||||
"Json" {
|
||||
if((Test-Path -LiteralPath $ResolvedOutputPath -PathType Leaf) -and (-not $Force)){
|
||||
throw "File [$ResolvedOutputPath] already exists. Use -Force to overwrite it."
|
||||
}
|
||||
|
||||
$Parent = Split-Path -Path $ResolvedOutputPath -Parent
|
||||
if(-not [string]::IsNullOrWhiteSpace($Parent) -and -not (Test-Path -LiteralPath $Parent)){
|
||||
New-Item -Path $Parent -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$ConfigurationData | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $ResolvedOutputPath -Encoding UTF8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($PassThru){
|
||||
return [PSCustomObject]@{
|
||||
Path = $ResolvedOutputPath
|
||||
Files = $MergeResult.Files
|
||||
ConfigurationData = $ConfigurationData
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,4 +117,175 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
||||
|
||||
{ Merge-DSCConfigurationData -Path $APath } | Should Throw
|
||||
}
|
||||
|
||||
It 'exports importable PSD1 files with quoted metadata keys' {
|
||||
$Path = Join-Path -Path $TestDrive -ChildPath 'Export.psd1'
|
||||
|
||||
@{
|
||||
Metadata = @{
|
||||
Description = @{
|
||||
'de-DE' = 'Deutsch'
|
||||
'en-US' = 'English'
|
||||
}
|
||||
}
|
||||
} | Export-PowerShellDataFile -Path $Path -Force
|
||||
|
||||
$Data = Import-PowerShellDataFile -Path $Path
|
||||
$Data.Metadata.Description['de-DE'] | Should Be 'Deutsch'
|
||||
$Data.Metadata.Description['en-US'] | Should Be 'English'
|
||||
}
|
||||
|
||||
It 'creates materialized deployment data without exporting by default' {
|
||||
$DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1'
|
||||
$ChildPath = Join-Path -Path $TestDrive -ChildPath 'Contoso.psd1'
|
||||
|
||||
@'
|
||||
@{
|
||||
Metadata = @{
|
||||
TemplateType = 'Environment'
|
||||
Name = 'Default'
|
||||
}
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Type = 'string'
|
||||
Required = $true
|
||||
}
|
||||
}
|
||||
Resources = @{
|
||||
AllNodes = @(
|
||||
@{
|
||||
NodeName = '*'
|
||||
PSDSCAllowPlainTextPassword = $true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
@'
|
||||
@{
|
||||
Metadata = @{
|
||||
TemplateType = 'Environment'
|
||||
Name = 'Contoso'
|
||||
Extends = './Default.psd1'
|
||||
}
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Value = 'contoso.local'
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $ChildPath -Encoding UTF8
|
||||
|
||||
$Result = New-DSCConfigurationDataDeployment `
|
||||
-Name 'Contoso-Test' `
|
||||
-DeploymentId '123' `
|
||||
-SourceTemplatePath $ChildPath `
|
||||
-AllNodes @(
|
||||
@{
|
||||
NodeName = 'Node01'
|
||||
RunCentralAdministration = $true
|
||||
}
|
||||
)
|
||||
|
||||
$Result.Metadata.TemplateType | Should Be 'Deployment'
|
||||
$Result.Metadata.Sources.Files.Count | Should Be 2
|
||||
$Result.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||
$Result.Resources.AllNodes[0].NodeName | Should Be 'Node01'
|
||||
$Result.Resources.AllNodes[0].PSDSCAllowPlainTextPassword | Should Be $true
|
||||
}
|
||||
|
||||
It 'exports materialized deployment data as PSD1' {
|
||||
$DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1'
|
||||
$ChildPath = Join-Path -Path $TestDrive -ChildPath 'Contoso.psd1'
|
||||
$DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployments\Deployment_123.psd1'
|
||||
|
||||
@'
|
||||
@{
|
||||
Metadata = @{
|
||||
TemplateType = 'Environment'
|
||||
Name = 'Default'
|
||||
}
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Type = 'string'
|
||||
Required = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
@'
|
||||
@{
|
||||
Metadata = @{
|
||||
TemplateType = 'Environment'
|
||||
Name = 'Contoso'
|
||||
Extends = './Default.psd1'
|
||||
}
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Value = 'contoso.local'
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $ChildPath -Encoding UTF8
|
||||
|
||||
$Result = New-DSCConfigurationDataDeployment `
|
||||
-Path $DeploymentPath `
|
||||
-Name 'Contoso-Test' `
|
||||
-DeploymentId '123' `
|
||||
-SourceTemplatePath $ChildPath `
|
||||
-AllNodes @(
|
||||
@{
|
||||
NodeName = 'Node01'
|
||||
RunCentralAdministration = $true
|
||||
}
|
||||
) `
|
||||
-UseRelativePaths `
|
||||
-Export `
|
||||
-Format PowerShellDataFile `
|
||||
-Force `
|
||||
-PassThru
|
||||
|
||||
$DeploymentPath | Should Exist
|
||||
$Result.ConfigurationData.Metadata.TemplateType | Should Be 'Deployment'
|
||||
$Result.ConfigurationData.Metadata.Sources.Files.Count | Should Be 2
|
||||
$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
|
||||
|
||||
$Imported = Import-PowerShellDataFile -Path $DeploymentPath
|
||||
$Imported.Metadata.Name | Should Be 'Contoso-Test'
|
||||
$Imported.Metadata.Sources.Templates.Count | Should Be 1
|
||||
}
|
||||
|
||||
It 'exports materialized deployment data as JSON' {
|
||||
$DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1'
|
||||
$DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployment_123.json'
|
||||
|
||||
@'
|
||||
@{
|
||||
Parameters = @{
|
||||
DomainFQDN = @{
|
||||
Type = 'string'
|
||||
Value = 'contoso.local'
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||
|
||||
New-DSCConfigurationDataDeployment `
|
||||
-Path $DeploymentPath `
|
||||
-Name 'Contoso-Test' `
|
||||
-DeploymentId '123' `
|
||||
-SourceTemplatePath $DefaultPath `
|
||||
-Export `
|
||||
-Format Json `
|
||||
-Force
|
||||
|
||||
$DeploymentPath | Should Exist
|
||||
$Imported = Get-Content -Path $DeploymentPath -Raw | ConvertFrom-Json
|
||||
$Imported.Metadata.TemplateType | Should Be 'Deployment'
|
||||
$Imported.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user