commit 4b83abc7f24ac9e99797a196887477cab0cf4d35 Author: Torsten Brendgen Date: Sat Jun 27 00:36:04 2026 +0200 initial Commit diff --git a/.tests/Resolve-DSCConfigurationData.Tests.ps1 b/.tests/Resolve-DSCConfigurationData.Tests.ps1 new file mode 100644 index 0000000..4b886b3 --- /dev/null +++ b/.tests/Resolve-DSCConfigurationData.Tests.ps1 @@ -0,0 +1,363 @@ +$script:ModuleRoot = Split-Path -Path $PSScriptRoot -Parent +Import-Module (Join-Path -Path $script:ModuleRoot -ChildPath "Resolve-DSCConfigurationData.psd1") -Force + +Describe "Resolve-DSCConfigurationData" { + It "resolves parameters using Value before DefaultValue" { + $ConfigurationData = @{ + Parameters = @{ + Name = @{ + DefaultValue = "Default" + Value = "Deployment" + } + } + Resources = @{ + Value = "[parameters('Name')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Value | Should Be "Deployment" + } + + It "allows parameter values listed in AllowedValues" { + $ConfigurationData = @{ + Parameters = @{ + Stage = @{ + Value = "Test" + AllowedValues = @( + "Install", + "Test", + "Release" + ) + } + } + Resources = @{ + Stage = "[parameters('Stage')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Stage | Should Be "Test" + } + + It "validates DefaultValue against AllowedValues when Value is not set" { + $ConfigurationData = @{ + Parameters = @{ + Stage = @{ + DefaultValue = "Release" + AllowedValues = @( + "Install", + "Test", + "Release" + ) + } + } + Resources = @{ + Stage = "[parameters('Stage')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Stage | Should Be "Release" + } + + It "throws when a parameter value is not listed in AllowedValues" { + $ConfigurationData = @{ + Parameters = @{ + Stage = @{ + Value = "Production" + AllowedValues = @( + "Install", + "Test", + "Release" + ) + } + } + Resources = @{ + Stage = "[parameters('Stage')]" + } + } + + { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw + } + + It "validates required parameters" { + $ConfigurationData = @{ + Parameters = @{ + DatabasePrefix = @{ + Type = "string" + Required = $true + } + } + } + + { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw + } + + It "validates parameter length constraints" { + $ConfigurationData = @{ + Parameters = @{ + DatabasePrefix = @{ + Value = "S" + MinLength = 2 + MaxLength = 10 + } + } + } + + { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw + } + + It "validates parameter patterns" { + $ConfigurationData = @{ + Parameters = @{ + DatabasePrefix = @{ + Value = "Share Point" + Pattern = "^[A-Za-z][A-Za-z0-9_-]*$" + } + } + } + + { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw + } + + It "supports localized deprecated parameter messages" { + $ConfigurationData = @{ + Parameters = @{ + OldDatabasePrefix = @{ + Value = "SharePoint" + Deprecated = @{ + Message = @{ + "de-DE" = "Der Parameter [OldDatabasePrefix] ist veraltet. Verwende [DatabasePrefix]." + "en-US" = "Parameter [OldDatabasePrefix] is deprecated. Use [DatabasePrefix]." + } + } + } + } + Resources = @{ + Value = "[parameters('OldDatabasePrefix')]" + } + } + + $Warnings = @() + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData -WarningVariable Warnings 3>$null + + $Result.Resources.Value | Should Be "SharePoint" + $Warnings.Count | Should Be 1 + ([string]$Warnings[0]) | Should Match "OldDatabasePrefix" + } + + It "resolves variables that reference parameters" { + $ConfigurationData = @{ + Parameters = @{ + Prefix = @{ + Value = "SharePoint" + } + } + Variables = @{ + ConfigDbName = "[concat(parameters('Prefix'), '_Farm_Config')]" + } + Resources = @{ + DatabaseName = "[variables('ConfigDbName')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Variables.ConfigDbName | Should Be "SharePoint_Farm_Config" + $Result.Resources.DatabaseName | Should Be "SharePoint_Farm_Config" + } + + It "resolves nested functions" { + $ConfigurationData = @{ + Parameters = @{ + Prefix = @{ + Value = "sharepoint" + } + } + Resources = @{ + Name = "[concat(toUpper(parameters('Prefix')), '_Services')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Name | Should Be "SHAREPOINT_Services" + } + + It "resolves string helper functions" { + $ConfigurationData = @{ + Resources = @{ + Lower = "[toLower('BGW-LAN')]" + Upper = "[toUpper('bgw-lan')]" + First = "[firstIndexOf('a', 'b', 'c')]" + Last = "[lastIndexOf('a', 'b', 'c')]" + Index = "[indexOf('BGW', 1)]" + Substring = "[substring('SharePoint', 5, 5)]" + Replace = "[replace('BGW-LAN', '-', '_')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Lower | Should Be "bgw-lan" + $Result.Resources.Upper | Should Be "BGW-LAN" + $Result.Resources.First | Should Be "a" + $Result.Resources.Last | Should Be "c" + $Result.Resources.Index | Should Be "G" + $Result.Resources.Substring | Should Be "Point" + $Result.Resources.Replace | Should Be "BGW_LAN" + } + + It "resolves formatting, fallback, and conditional functions" { + $ConfigurationData = @{ + Parameters = @{ + DatabasePrefix = @{ + Value = "SharePoint" + } + DomainLabel = @{ + Value = "LAN" + } + Stage = @{ + Value = "Test" + } + OptionalValue = @{ + Value = "" + } + } + Resources = @{ + Formatted = "[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Stage'))]" + Fallback = "[coalesce(parameters('OptionalValue'), 'DefaultValue')]" + Conditional = "[if(equals(parameters('Stage'), 'Test'), 'TST', 'PRD')]" + NotEquals = "[notEquals(parameters('Stage'), 'Prod')]" + AndValue = "[and(equals(parameters('Stage'), 'Test'), not(empty(parameters('DatabasePrefix'))))]" + OrValue = "[or(equals(parameters('Stage'), 'Prod'), equals(parameters('Stage'), 'Test'))]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Formatted | Should Be "SharePoint_LAN_Test" + $Result.Resources.Fallback | Should Be "DefaultValue" + $Result.Resources.Conditional | Should Be "TST" + $Result.Resources.NotEquals | Should Be $true + $Result.Resources.AndValue | Should Be $true + $Result.Resources.OrValue | Should Be $true + } + + It "resolves collection and string inspection functions" { + $ConfigurationData = @{ + Parameters = @{ + DomainFQDN = @{ + Value = "bgw-online.de" + } + } + Resources = @{ + Split = "[split(parameters('DomainFQDN'), '.')]" + Joined = "[join(split(parameters('DomainFQDN'), '.'), '_')]" + JoinedNotEmpty = "[joinNotEmpty('_', 'SharePoint', '', 'LAN', 'Test', '', 'Services')]" + Take = "[join(take(split(parameters('DomainFQDN'), '.'), 1), '_')]" + Skip = "[join(skip(split(parameters('DomainFQDN'), '.'), 1), '_')]" + First = "[first(split(parameters('DomainFQDN'), '.'))]" + Last = "[last(split(parameters('DomainFQDN'), '.'))]" + Unique = "[join(unique(split('SP.SP.SQL', '.')), '_')]" + Sort = "[join(sort(split('SQL.SP.APP', '.')), '_')]" + ContainsString = "[contains(parameters('DomainFQDN'), 'online')]" + ContainsArray = "[contains(split(parameters('DomainFQDN'), '.'), 'de')]" + StartsWith = "[startsWith(parameters('DomainFQDN'), 'bgw')]" + EndsWith = "[endsWith(parameters('DomainFQDN'), 'DE')]" + Length = "[length(split(parameters('DomainFQDN'), '.'))]" + Empty = "[empty('')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Split[0] | Should Be "bgw-online" + $Result.Resources.Split[1] | Should Be "de" + $Result.Resources.Joined | Should Be "bgw-online_de" + $Result.Resources.JoinedNotEmpty | Should Be "SharePoint_LAN_Test_Services" + $Result.Resources.Take | Should Be "bgw-online" + $Result.Resources.Skip | Should Be "de" + $Result.Resources.First | Should Be "bgw-online" + $Result.Resources.Last | Should Be "de" + $Result.Resources.Unique | Should Be "SP_SQL" + $Result.Resources.Sort | Should Be "APP_SP_SQL" + $Result.Resources.ContainsString | Should Be $true + $Result.Resources.ContainsArray | Should Be $true + $Result.Resources.StartsWith | Should Be $true + $Result.Resources.EndsWith | Should Be $true + $Result.Resources.Length | Should Be 2 + $Result.Resources.Empty | Should Be $true + } + + It "resolves trim and padding functions" { + $ConfigurationData = @{ + Resources = @{ + Trim = "[trim(' SharePoint ')]" + TrimStart = "[trimStart(' SharePoint')]" + TrimEnd = "[trimEnd('SharePoint ')]" + PadLeft = "[padLeft('1', 2, '0')]" + PadRight = "[padRight('SP', 4, '0')]" + DefaultIfEmpty = "[defaultIfEmpty('', 'SharePoint')]" + Sanitized = "[sanitizeName(' SharePoint LAN/Test DB ')]" + Normalized = "[normalizeSeparator('__SharePoint___LAN_Test__', '_')]" + Prefix = "[prefixIfNotEmpty('LAN', 'BGW-')]" + EmptyPrefix = "[prefixIfNotEmpty('', 'BGW-')]" + Suffix = "[suffixIfNotEmpty('SharePoint', '_DB')]" + EmptySuffix = "[suffixIfNotEmpty('', '_DB')]" + } + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.Resources.Trim | Should Be "SharePoint" + $Result.Resources.TrimStart | Should Be "SharePoint" + $Result.Resources.TrimEnd | Should Be "SharePoint" + $Result.Resources.PadLeft | Should Be "01" + $Result.Resources.PadRight | Should Be "SP00" + $Result.Resources.DefaultIfEmpty | Should Be "SharePoint" + $Result.Resources.Sanitized | Should Be "SharePoint_LAN_Test_DB" + $Result.Resources.Normalized | Should Be "SharePoint_LAN_Test" + $Result.Resources.Prefix | Should Be "BGW-LAN" + $Result.Resources.EmptyPrefix | Should Be "" + $Result.Resources.Suffix | Should Be "SharePoint_DB" + $Result.Resources.EmptySuffix | Should Be "" + } + + It "resolves values inside arrays recursively" { + $ConfigurationData = @{ + Parameters = @{ + NodeName = @{ + Value = "SP01" + } + } + AllNodes = @( + @{ + NodeName = "[parameters('NodeName')]" + } + ) + } + + $Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData + + $Result.AllNodes[0].NodeName | Should Be "SP01" + } + + It "throws for circular variables" { + $ConfigurationData = @{ + Variables = @{ + A = "[variables('B')]" + B = "[variables('A')]" + } + Resources = @{ + Value = "[variables('A')]" + } + } + + { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw + } +} diff --git a/Private/Assert-ConfigurationDataExpressionArgumentCount.ps1 b/Private/Assert-ConfigurationDataExpressionArgumentCount.ps1 new file mode 100644 index 0000000..ea35b40 --- /dev/null +++ b/Private/Assert-ConfigurationDataExpressionArgumentCount.ps1 @@ -0,0 +1,20 @@ +function Assert-ConfigurationDataExpressionArgumentCount { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [AllowNull()] + [object[]] + $Arguments, + + [Parameter(Mandatory=$true)] + [int] + $Count + ) + + if(@($Arguments).Count -ne $Count){ + throw "Function [$Name] expects [$Count] argument(s), but received [$(@($Arguments).Count)]." + } +} diff --git a/Private/Assert-ConfigurationDataExpressionMinimumArgumentCount.ps1 b/Private/Assert-ConfigurationDataExpressionMinimumArgumentCount.ps1 new file mode 100644 index 0000000..bd7fdbe --- /dev/null +++ b/Private/Assert-ConfigurationDataExpressionMinimumArgumentCount.ps1 @@ -0,0 +1,20 @@ +function Assert-ConfigurationDataExpressionMinimumArgumentCount { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [AllowNull()] + [object[]] + $Arguments, + + [Parameter(Mandatory=$true)] + [int] + $Count + ) + + if(@($Arguments).Count -lt $Count){ + throw "Function [$Name] expects at least [$Count] argument(s), but received [$(@($Arguments).Count)]." + } +} diff --git a/Private/Assert-ConfigurationDataParameter.ps1 b/Private/Assert-ConfigurationDataParameter.ps1 new file mode 100644 index 0000000..1b67847 --- /dev/null +++ b/Private/Assert-ConfigurationDataParameter.ps1 @@ -0,0 +1,67 @@ +function Assert-ConfigurationDataParameter { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true)] + $Definition, + + [AllowNull()] + $Value, + + [Parameter(Mandatory=$true)] + [bool] + $HasValue + ) + + if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "Deprecated"){ + $Deprecated = Get-ConfigurationDataMapValue -Map $Definition -Key "Deprecated" + $Message = "Parameter [$Name] is deprecated." + + if((Test-ConfigurationDataMap -Value $Deprecated) -and (Test-ConfigurationDataMapContainsKey -Map $Deprecated -Key "Message")){ + $Message = Resolve-ConfigurationDataLocalizedText -Value (Get-ConfigurationDataMapValue -Map $Deprecated -Key "Message") -DefaultValue $Message + }elseif($Deprecated -is [string] -and -not [string]::IsNullOrWhiteSpace($Deprecated)){ + $Message = $Deprecated + } + + Write-Warning $Message + } + + $Required = $false + if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "Required"){ + $Required = [bool](Get-ConfigurationDataMapValue -Map $Definition -Key "Required") + } + + if($Required -and ((-not $HasValue) -or (Test-ConfigurationDataValueIsEmpty -Value $Value))){ + throw "Parameter [$Name] is required." + } + + if(-not $HasValue -or $null -eq $Value){ + return + } + + Assert-ConfigurationDataParameterAllowedValue -Name $Name -Definition $Definition -Value $Value + + if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MinLength"){ + $MinLength = [int](Get-ConfigurationDataMapValue -Map $Definition -Key "MinLength") + if(([string]$Value).Length -lt $MinLength){ + throw "Parameter [$Name] value length [$(([string]$Value).Length)] is less than MinLength [$MinLength]." + } + } + + if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MaxLength"){ + $MaxLength = [int](Get-ConfigurationDataMapValue -Map $Definition -Key "MaxLength") + if(([string]$Value).Length -gt $MaxLength){ + throw "Parameter [$Name] value length [$(([string]$Value).Length)] is greater than MaxLength [$MaxLength]." + } + } + + if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "Pattern"){ + $Pattern = [string](Get-ConfigurationDataMapValue -Map $Definition -Key "Pattern") + if(([string]$Value) -notmatch $Pattern){ + throw "Parameter [$Name] value [$Value] does not match pattern [$Pattern]." + } + } +} diff --git a/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 b/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 new file mode 100644 index 0000000..076bc0b --- /dev/null +++ b/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 @@ -0,0 +1,27 @@ +function Assert-ConfigurationDataParameterAllowedValue { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true)] + $Definition, + + [AllowNull()] + $Value + ) + + if(-not (Test-ConfigurationDataMapContainsKey -Map $Definition -Key "AllowedValues")){ + return + } + + $AllowedValues = @(Get-ConfigurationDataMapValue -Map $Definition -Key "AllowedValues") + if($AllowedValues.Count -eq 0){ + return + } + + if($AllowedValues -notcontains $Value){ + throw "Parameter [$Name] value [$Value] is not allowed. Allowed values are: $($AllowedValues -join ', ')." + } +} diff --git a/Private/ConvertTo-ConfigurationDataExpressionBoolean.ps1 b/Private/ConvertTo-ConfigurationDataExpressionBoolean.ps1 new file mode 100644 index 0000000..b02a8bc --- /dev/null +++ b/Private/ConvertTo-ConfigurationDataExpressionBoolean.ps1 @@ -0,0 +1,33 @@ +function ConvertTo-ConfigurationDataExpressionBoolean { + [CmdletBinding()] + Param( + [AllowNull()] + $Value + ) + + if($null -eq $Value){ + return $false + } + + if($Value -is [bool]){ + return $Value + } + + if($Value -is [int]){ + return $Value -ne 0 + } + + if($Value -is [string]){ + if($Value -match "^(?i:true|false)$"){ + return [bool]::Parse($Value) + } + + return $Value.Length -gt 0 + } + + if($Value -is [System.Array]){ + return @($Value).Count -gt 0 + } + + return [bool]$Value +} diff --git a/Private/ConvertTo-ConfigurationDataSafeName.ps1 b/Private/ConvertTo-ConfigurationDataSafeName.ps1 new file mode 100644 index 0000000..ded46a7 --- /dev/null +++ b/Private/ConvertTo-ConfigurationDataSafeName.ps1 @@ -0,0 +1,19 @@ +function ConvertTo-ConfigurationDataSafeName { + [CmdletBinding()] + Param( + [AllowNull()] + [string] + $Value, + + [Parameter(Mandatory=$true)] + [string] + $Separator + ) + + if($null -eq $Value){ + return "" + } + + $SafeName = [regex]::Replace($Value.Trim(), "[^A-Za-z0-9_-]+", $Separator) + return Normalize-ConfigurationDataSeparator -Value $SafeName -Separator $Separator +} diff --git a/Private/Get-ConfigurationDataMapValue.ps1 b/Private/Get-ConfigurationDataMapValue.ps1 new file mode 100644 index 0000000..65a892a --- /dev/null +++ b/Private/Get-ConfigurationDataMapValue.ps1 @@ -0,0 +1,13 @@ +function Get-ConfigurationDataMapValue { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + $Map, + + [Parameter(Mandatory=$true)] + [string] + $Key + ) + + return $Map[$Key] +} diff --git a/Private/Invoke-ConfigurationDataExpression.ps1 b/Private/Invoke-ConfigurationDataExpression.ps1 new file mode 100644 index 0000000..0f5b4ca --- /dev/null +++ b/Private/Invoke-ConfigurationDataExpression.ps1 @@ -0,0 +1,38 @@ +function Invoke-ConfigurationDataExpression { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Expression, + + [Parameter(Mandatory=$true)] + $Context + ) + + $Expression = $Expression.Trim() + + if($Expression -match "^'(.*)'$"){ + return $Matches[1] + } + + if($Expression -match "^-?\d+$"){ + return [int]$Expression + } + + if($Expression -match "^(?i:true|false)$"){ + return [bool]::Parse($Expression) + } + + if($Expression -notmatch "^([A-Za-z][A-Za-z0-9]*)\((.*)\)$"){ + throw "Invalid configuration data expression [$Expression]." + } + + $FunctionName = $Matches[1] + $ArgumentText = $Matches[2] + $Arguments = @() + foreach($Argument in @(Split-ConfigurationDataExpressionArguments -ArgumentText $ArgumentText)){ + $Arguments += ,(Invoke-ConfigurationDataExpressionArgument -Argument $Argument -Context $Context) + } + + return Invoke-ConfigurationDataExpressionFunction -Name $FunctionName -Arguments $Arguments -Context $Context +} diff --git a/Private/Invoke-ConfigurationDataExpressionArgument.ps1 b/Private/Invoke-ConfigurationDataExpressionArgument.ps1 new file mode 100644 index 0000000..4d34686 --- /dev/null +++ b/Private/Invoke-ConfigurationDataExpressionArgument.ps1 @@ -0,0 +1,35 @@ +function Invoke-ConfigurationDataExpressionArgument { + [CmdletBinding()] + Param( + [AllowNull()] + [string] + $Argument, + + [Parameter(Mandatory=$true)] + $Context + ) + + if($null -eq $Argument){ + return $null + } + + $Argument = $Argument.Trim() + + if($Argument -match "^'(.*)'$"){ + return $Matches[1] + } + + if($Argument -match "^-?\d+$"){ + return [int]$Argument + } + + if($Argument -match "^(?i:true|false)$"){ + return [bool]::Parse($Argument) + } + + if($Argument -match "^[A-Za-z][A-Za-z0-9]*\(.*\)$"){ + return Invoke-ConfigurationDataExpression -Expression $Argument -Context $Context + } + + return $Argument +} diff --git a/Private/Invoke-ConfigurationDataExpressionFunction.ps1 b/Private/Invoke-ConfigurationDataExpressionFunction.ps1 new file mode 100644 index 0000000..1eeef40 --- /dev/null +++ b/Private/Invoke-ConfigurationDataExpressionFunction.ps1 @@ -0,0 +1,279 @@ +function Invoke-ConfigurationDataExpressionFunction { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [AllowNull()] + [object[]] + $Arguments, + + [Parameter(Mandatory=$true)] + $Context + ) + + $FunctionName = $Name.ToLowerInvariant() + + switch($FunctionName){ + "parameters" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return Resolve-ConfigurationDataParameter -Name ([string]$Arguments[0]) -Context $Context + } + "variables" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return Resolve-ConfigurationDataVariable -Name ([string]$Arguments[0]) -Context $Context + } + "concat" { + return (@($Arguments) | ForEach-Object { [string]$_ }) -join "" + } + "format" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return [string]::Format([string]$Arguments[0], @($Arguments | Select-Object -Skip 1)) + } + "coalesce" { + foreach($Argument in @($Arguments)){ + if($null -ne $Argument -and [string]$Argument -ne ""){ + return $Argument + } + } + + return $null + } + "defaultifempty" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + if(Test-ConfigurationDataValueIsEmpty -Value $Arguments[0]){ + return $Arguments[1] + } + + return $Arguments[0] + } + "if" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 3 + if(ConvertTo-ConfigurationDataExpressionBoolean -Value $Arguments[0]){ + return $Arguments[1] + } + + return $Arguments[2] + } + "equals" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return $Arguments[0] -eq $Arguments[1] + } + "notequals" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return $Arguments[0] -ne $Arguments[1] + } + "and" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + foreach($Argument in @($Arguments)){ + if(-not (ConvertTo-ConfigurationDataExpressionBoolean -Value $Argument)){ + return $false + } + } + + return $true + } + "or" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + foreach($Argument in @($Arguments)){ + if(ConvertTo-ConfigurationDataExpressionBoolean -Value $Argument){ + return $true + } + } + + return $false + } + "not" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return -not (ConvertTo-ConfigurationDataExpressionBoolean -Value $Arguments[0]) + } + "tolower" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ([string]$Arguments[0]).ToLowerInvariant() + } + "toupper" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ([string]$Arguments[0]).ToUpperInvariant() + } + "firstindexof" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return $Arguments[0] + } + "lastindexof" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return $Arguments[$Arguments.Count - 1] + } + "indexof" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ([string]$Arguments[0])[([int]$Arguments[1])].ToString() + } + "substring" { + if($Arguments.Count -eq 2){ + return ([string]$Arguments[0]).Substring([int]$Arguments[1]) + } + + if($Arguments.Count -eq 3){ + return ([string]$Arguments[0]).Substring([int]$Arguments[1], [int]$Arguments[2]) + } + + throw "Function [$Name] expects 2 or 3 arguments, but received [$($Arguments.Count)]." + } + "replace" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 3 + return ([string]$Arguments[0]).Replace([string]$Arguments[1], [string]$Arguments[2]) + } + "sanitizeName" { + if($Arguments.Count -eq 1){ + return ConvertTo-ConfigurationDataSafeName -Value ([string]$Arguments[0]) -Separator "_" + } + + if($Arguments.Count -eq 2){ + return ConvertTo-ConfigurationDataSafeName -Value ([string]$Arguments[0]) -Separator ([string]$Arguments[1]) + } + + throw "Function [$Name] expects 1 or 2 arguments, but received [$($Arguments.Count)]." + } + "normalizeseparator" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return Normalize-ConfigurationDataSeparator -Value ([string]$Arguments[0]) -Separator ([string]$Arguments[1]) + } + "prefixifnotempty" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + if(Test-ConfigurationDataValueIsEmpty -Value $Arguments[0]){ + return "" + } + + return ([string]$Arguments[1]) + ([string]$Arguments[0]) + } + "suffixifnotempty" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + if(Test-ConfigurationDataValueIsEmpty -Value $Arguments[0]){ + return "" + } + + return ([string]$Arguments[0]) + ([string]$Arguments[1]) + } + "contains" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + if($Arguments[0] -is [System.Array] -and $Arguments[0] -isnot [string]){ + return @($Arguments[0]) -contains $Arguments[1] + } + + return ([string]$Arguments[0]).Contains([string]$Arguments[1]) + } + "startswith" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ([string]$Arguments[0]).StartsWith([string]$Arguments[1], [System.StringComparison]::OrdinalIgnoreCase) + } + "endswith" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ([string]$Arguments[0]).EndsWith([string]$Arguments[1], [System.StringComparison]::OrdinalIgnoreCase) + } + "split" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ,(([string]$Arguments[0]).Split([string[]]@([string]$Arguments[1]), [System.StringSplitOptions]::None)) + } + "join" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return (@($Arguments[0]) | ForEach-Object { [string]$_ }) -join ([string]$Arguments[1]) + } + "joinnotempty" { + Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $Name -Arguments $Arguments -Count 1 + $Separator = [string]$Arguments[0] + $Items = @() + + foreach($Argument in @($Arguments | Select-Object -Skip 1)){ + foreach($Item in @($Argument)){ + if($null -ne $Item -and -not [string]::IsNullOrWhiteSpace([string]$Item)){ + $Items += [string]$Item + } + } + } + + return $Items -join $Separator + } + "take" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ,(@($Arguments[0]) | Select-Object -First ([int]$Arguments[1])) + } + "skip" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2 + return ,(@($Arguments[0]) | Select-Object -Skip ([int]$Arguments[1])) + } + "first" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return @($Arguments[0])[0] + } + "last" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + $Items = @($Arguments[0]) + return $Items[$Items.Count - 1] + } + "unique" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ,(@($Arguments[0]) | Select-Object -Unique) + } + "sort" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ,(@($Arguments[0]) | Sort-Object) + } + "trim" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ([string]$Arguments[0]).Trim() + } + "trimstart" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ([string]$Arguments[0]).TrimStart() + } + "trimend" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + return ([string]$Arguments[0]).TrimEnd() + } + "padleft" { + if($Arguments.Count -eq 2){ + return ([string]$Arguments[0]).PadLeft([int]$Arguments[1]) + } + + if($Arguments.Count -eq 3){ + return ([string]$Arguments[0]).PadLeft([int]$Arguments[1], ([string]$Arguments[2])[0]) + } + + throw "Function [$Name] expects 2 or 3 arguments, but received [$($Arguments.Count)]." + } + "padright" { + if($Arguments.Count -eq 2){ + return ([string]$Arguments[0]).PadRight([int]$Arguments[1]) + } + + if($Arguments.Count -eq 3){ + return ([string]$Arguments[0]).PadRight([int]$Arguments[1], ([string]$Arguments[2])[0]) + } + + throw "Function [$Name] expects 2 or 3 arguments, but received [$($Arguments.Count)]." + } + "length" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + if($Arguments[0] -is [System.Array] -and $Arguments[0] -isnot [string]){ + return @($Arguments[0]).Count + } + + return ([string]$Arguments[0]).Length + } + "empty" { + Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1 + if($null -eq $Arguments[0]){ + return $true + } + + if($Arguments[0] -is [System.Array] -and $Arguments[0] -isnot [string]){ + return @($Arguments[0]).Count -eq 0 + } + + return [string]$Arguments[0] -eq "" + } + default { + throw "Unsupported configuration data expression function [$Name]." + } + } +} diff --git a/Private/New-ConfigurationDataResolutionContext.ps1 b/Private/New-ConfigurationDataResolutionContext.ps1 new file mode 100644 index 0000000..7257209 --- /dev/null +++ b/Private/New-ConfigurationDataResolutionContext.ps1 @@ -0,0 +1,44 @@ +function New-ConfigurationDataResolutionContext { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [System.Collections.Hashtable] + $ConfigurationData + ) + + $Context = [PSCustomObject]@{ + Parameters = @{} + Variables = @{} + VariableDefinitions = @{} + ResolvingVariables = @{} + } + + if($ConfigurationData.ContainsKey("Parameters") -and $null -ne $ConfigurationData.Parameters){ + foreach($Entry in $ConfigurationData.Parameters.GetEnumerator()){ + $Definition = $Entry.Value + + $HasExplicitValue = (Test-ConfigurationDataMap -Value $Definition) -and (Test-ConfigurationDataMapContainsKey -Map $Definition -Key "Value") + $HasDefaultValue = (Test-ConfigurationDataMap -Value $Definition) -and (Test-ConfigurationDataMapContainsKey -Map $Definition -Key "DefaultValue") + + if($HasExplicitValue){ + $Context.Parameters[$Entry.Name] = Get-ConfigurationDataMapValue -Map $Definition -Key "Value" + }elseif($HasDefaultValue){ + $Context.Parameters[$Entry.Name] = Get-ConfigurationDataMapValue -Map $Definition -Key "DefaultValue" + }else{ + $Context.Parameters[$Entry.Name] = $Definition + } + + if(Test-ConfigurationDataMap -Value $Definition){ + Assert-ConfigurationDataParameter -Name $Entry.Name -Definition $Definition -Value $Context.Parameters[$Entry.Name] -HasValue:($HasExplicitValue -or $HasDefaultValue) + } + } + } + + if($ConfigurationData.ContainsKey("Variables") -and $null -ne $ConfigurationData.Variables){ + foreach($Entry in $ConfigurationData.Variables.GetEnumerator()){ + $Context.VariableDefinitions[$Entry.Name] = $Entry.Value + } + } + + return $Context +} diff --git a/Private/Normalize-ConfigurationDataSeparator.ps1 b/Private/Normalize-ConfigurationDataSeparator.ps1 new file mode 100644 index 0000000..4260be9 --- /dev/null +++ b/Private/Normalize-ConfigurationDataSeparator.ps1 @@ -0,0 +1,26 @@ +function Normalize-ConfigurationDataSeparator { + [CmdletBinding()] + Param( + [AllowNull()] + [string] + $Value, + + [Parameter(Mandatory=$true)] + [string] + $Separator + ) + + if($null -eq $Value){ + return "" + } + + if([string]::IsNullOrEmpty($Separator)){ + return $Value + } + + $EscapedSeparator = [regex]::Escape($Separator) + $Normalized = [regex]::Replace($Value, "($EscapedSeparator){2,}", $Separator) + $Normalized = $Normalized.Trim() + $Normalized = [regex]::Replace($Normalized, "^$EscapedSeparator|$EscapedSeparator$", "") + return $Normalized +} diff --git a/Private/Resolve-ConfigurationDataExpression.ps1 b/Private/Resolve-ConfigurationDataExpression.ps1 new file mode 100644 index 0000000..d00f502 --- /dev/null +++ b/Private/Resolve-ConfigurationDataExpression.ps1 @@ -0,0 +1,16 @@ +function Resolve-ConfigurationDataExpression { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Expression, + + [Parameter(Mandatory=$true)] + $Context + ) + + $Body = $Expression.Trim() + $Body = $Body.Substring(1, $Body.Length - 2).Trim() + + return Invoke-ConfigurationDataExpression -Expression $Body -Context $Context +} diff --git a/Private/Resolve-ConfigurationDataLocalizedText.ps1 b/Private/Resolve-ConfigurationDataLocalizedText.ps1 new file mode 100644 index 0000000..e1e1aa2 --- /dev/null +++ b/Private/Resolve-ConfigurationDataLocalizedText.ps1 @@ -0,0 +1,50 @@ +function Resolve-ConfigurationDataLocalizedText { + [CmdletBinding()] + Param( + [AllowNull()] + $Value, + + [AllowNull()] + [string] + $DefaultValue + ) + + if($null -eq $Value){ + return $DefaultValue + } + + if($Value -is [string]){ + if([string]::IsNullOrWhiteSpace($Value)){ + return $DefaultValue + } + + return $Value + } + + if(-not (Test-ConfigurationDataMap -Value $Value)){ + return [string]$Value + } + + $CultureNames = @( + [System.Globalization.CultureInfo]::CurrentUICulture.Name, + [System.Globalization.CultureInfo]::CurrentCulture.Name, + "en-US", + "de-DE", + "en", + "de" + ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + + foreach($CultureName in $CultureNames){ + if(Test-ConfigurationDataMapContainsKey -Map $Value -Key $CultureName){ + return [string](Get-ConfigurationDataMapValue -Map $Value -Key $CultureName) + } + } + + foreach($Entry in $Value.GetEnumerator()){ + if($null -ne $Entry.Value -and -not [string]::IsNullOrWhiteSpace([string]$Entry.Value)){ + return [string]$Entry.Value + } + } + + return $DefaultValue +} diff --git a/Private/Resolve-ConfigurationDataParameter.ps1 b/Private/Resolve-ConfigurationDataParameter.ps1 new file mode 100644 index 0000000..42e9f6f --- /dev/null +++ b/Private/Resolve-ConfigurationDataParameter.ps1 @@ -0,0 +1,17 @@ +function Resolve-ConfigurationDataParameter { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true)] + $Context + ) + + if(-not $Context.Parameters.ContainsKey($Name)){ + throw "Parameter [$Name] is not defined." + } + + return Resolve-ConfigurationDataValue -Value $Context.Parameters[$Name] -Context $Context +} diff --git a/Private/Resolve-ConfigurationDataValue.ps1 b/Private/Resolve-ConfigurationDataValue.ps1 new file mode 100644 index 0000000..cee5990 --- /dev/null +++ b/Private/Resolve-ConfigurationDataValue.ps1 @@ -0,0 +1,44 @@ +function Resolve-ConfigurationDataValue { + [CmdletBinding()] + Param( + [AllowNull()] + $Value, + + [Parameter(Mandatory=$true)] + $Context + ) + + if($null -eq $Value){ + return $null + } + + if($Value -is [System.Collections.Specialized.OrderedDictionary]){ + $Resolved = [ordered]@{} + foreach($Entry in $Value.GetEnumerator()){ + $Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context + } + return $Resolved + } + + if($Value -is [System.Collections.Hashtable]){ + $Resolved = @{} + foreach($Entry in $Value.GetEnumerator()){ + $Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context + } + return $Resolved + } + + if($Value -is [System.Array] -and $Value -isnot [string]){ + $Resolved = @() + foreach($Item in $Value){ + $Resolved += ,(Resolve-ConfigurationDataValue -Value $Item -Context $Context) + } + return ,$Resolved + } + + if($Value -is [string] -and (Test-ConfigurationDataExpression -Value $Value)){ + return Resolve-ConfigurationDataExpression -Expression $Value -Context $Context + } + + return $Value +} diff --git a/Private/Resolve-ConfigurationDataVariable.ps1 b/Private/Resolve-ConfigurationDataVariable.ps1 new file mode 100644 index 0000000..a99f1de --- /dev/null +++ b/Private/Resolve-ConfigurationDataVariable.ps1 @@ -0,0 +1,32 @@ +function Resolve-ConfigurationDataVariable { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true)] + $Context + ) + + if($Context.Variables.ContainsKey($Name)){ + return $Context.Variables[$Name] + } + + if(-not $Context.VariableDefinitions.ContainsKey($Name)){ + throw "Variable [$Name] is not defined." + } + + if($Context.ResolvingVariables.ContainsKey($Name)){ + throw "Circular variable reference detected for variable [$Name]." + } + + $Context.ResolvingVariables[$Name] = $true + try { + $Resolved = Resolve-ConfigurationDataValue -Value $Context.VariableDefinitions[$Name] -Context $Context + $Context.Variables[$Name] = $Resolved + return $Resolved + } finally { + $Context.ResolvingVariables.Remove($Name) + } +} diff --git a/Private/Split-ConfigurationDataExpressionArguments.ps1 b/Private/Split-ConfigurationDataExpressionArguments.ps1 new file mode 100644 index 0000000..5369457 --- /dev/null +++ b/Private/Split-ConfigurationDataExpressionArguments.ps1 @@ -0,0 +1,47 @@ +function Split-ConfigurationDataExpressionArguments { + [CmdletBinding()] + Param( + [AllowNull()] + [string] + $ArgumentText + ) + + if([string]::IsNullOrWhiteSpace($ArgumentText)){ + return @() + } + + $Arguments = @() + $Current = New-Object -TypeName System.Text.StringBuilder + $Depth = 0 + $InString = $false + + for($Index = 0; $Index -lt $ArgumentText.Length; $Index++){ + $Character = $ArgumentText[$Index] + + if($Character -eq "'"){ + [void]$Current.Append($Character) + $InString = -not $InString + continue + } + + if(-not $InString){ + if($Character -eq "("){ + $Depth++ + }elseif($Character -eq ")"){ + $Depth-- + }elseif($Character -eq "," -and $Depth -eq 0){ + $Arguments += $Current.ToString().Trim() + [void]$Current.Clear() + continue + } + } + + [void]$Current.Append($Character) + } + + if($Current.Length -gt 0){ + $Arguments += $Current.ToString().Trim() + } + + return $Arguments +} diff --git a/Private/Test-ConfigurationDataExpression.ps1 b/Private/Test-ConfigurationDataExpression.ps1 new file mode 100644 index 0000000..39a325d --- /dev/null +++ b/Private/Test-ConfigurationDataExpression.ps1 @@ -0,0 +1,15 @@ +function Test-ConfigurationDataExpression { + [CmdletBinding()] + Param( + [AllowNull()] + [string] + $Value + ) + + if([string]::IsNullOrWhiteSpace($Value)){ + return $false + } + + $Trimmed = $Value.Trim() + return $Trimmed.StartsWith("[") -and $Trimmed.EndsWith("]") +} diff --git a/Private/Test-ConfigurationDataMap.ps1 b/Private/Test-ConfigurationDataMap.ps1 new file mode 100644 index 0000000..9b04257 --- /dev/null +++ b/Private/Test-ConfigurationDataMap.ps1 @@ -0,0 +1,9 @@ +function Test-ConfigurationDataMap { + [CmdletBinding()] + Param( + [AllowNull()] + $Value + ) + + return ($Value -is [System.Collections.Hashtable]) -or ($Value -is [System.Collections.Specialized.OrderedDictionary]) +} diff --git a/Private/Test-ConfigurationDataMapContainsKey.ps1 b/Private/Test-ConfigurationDataMapContainsKey.ps1 new file mode 100644 index 0000000..22f7b7b --- /dev/null +++ b/Private/Test-ConfigurationDataMapContainsKey.ps1 @@ -0,0 +1,21 @@ +function Test-ConfigurationDataMapContainsKey { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + $Map, + + [Parameter(Mandatory=$true)] + [string] + $Key + ) + + if($Map -is [System.Collections.Hashtable]){ + return $Map.ContainsKey($Key) + } + + if($Map -is [System.Collections.Specialized.OrderedDictionary]){ + return $Map.Contains($Key) + } + + return $false +} diff --git a/Private/Test-ConfigurationDataValueIsEmpty.ps1 b/Private/Test-ConfigurationDataValueIsEmpty.ps1 new file mode 100644 index 0000000..0aa5875 --- /dev/null +++ b/Private/Test-ConfigurationDataValueIsEmpty.ps1 @@ -0,0 +1,21 @@ +function Test-ConfigurationDataValueIsEmpty { + [CmdletBinding()] + Param( + [AllowNull()] + $Value + ) + + if($null -eq $Value){ + return $true + } + + if($Value -is [string]){ + return [string]::IsNullOrWhiteSpace($Value) + } + + if($Value -is [System.Array]){ + return @($Value).Count -eq 0 + } + + return $false +} diff --git a/Public/Resolve-DSCConfigurationData.ps1 b/Public/Resolve-DSCConfigurationData.ps1 new file mode 100644 index 0000000..0d75c96 --- /dev/null +++ b/Public/Resolve-DSCConfigurationData.ps1 @@ -0,0 +1,13 @@ +function Resolve-DSCConfigurationData { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [System.Collections.Hashtable] + $ConfigurationData + ) + + process { + $Context = New-ConfigurationDataResolutionContext -ConfigurationData $ConfigurationData + return Resolve-ConfigurationDataValue -Value $ConfigurationData -Context $Context + } +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..d9e8ecc --- /dev/null +++ b/Readme.md @@ -0,0 +1,396 @@ +# Resolve-DSCConfigurationData + +`Resolve-DSCConfigurationData` resolves parameter, variable, and expression references in DSC configuration data. + +Typical flow: + +```powershell +$merged = Merge-DSCConfigurationData -Template $service -Deployment $environment +$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged +``` + +Expressions use an ARM-like syntax: + +```powershell +"[parameters('DatabasePrefix')]" +"[variables('ServiceDbPrefix')]" +"[concat(parameters('DatabasePrefix'), '_', parameters('ServiceDatabaseSegment'))]" +``` + +## Parameter Example + +This block shows all currently supported parameter properties. + +```powershell +@{ + Parameters = @{ + DatabasePrefix = @{ + Type = 'string' + + # Optional explicit value. If Value is present, it wins over DefaultValue. + Value = 'SharePoint' + + # Used when Value is not present. + DefaultValue = 'SharePoint' + + # If true, Value or DefaultValue must be present and not empty. + Required = $true + + # Optional fixed set of valid values. + AllowedValues = @( + 'SharePoint', + 'ProjectServer', + 'Search' + ) + + # Optional string length validation. + MinLength = 2 + MaxLength = 32 + + # Optional regex validation. + Pattern = '^[A-Za-z][A-Za-z0-9_-]*$' + + # Metadata for later reporting/output tooling. The resolver does not mask values yet. + Sensitive = $false + + # Emits a warning when the parameter is present. + Deprecated = @{ + Message = @{ + 'de-DE' = 'Der Parameter [DatabasePrefix] ist veraltet. Verwende [SharePointDatabasePrefix].' + 'en-US' = 'Parameter [DatabasePrefix] is deprecated. Use [SharePointDatabasePrefix].' + } + } + + Metadata = @{ + Description = @{ + 'de-DE' = 'Praefix fuer alle von der SharePoint-Farm angelegten Datenbanken.' + 'en-US' = 'Prefix for all databases created by the SharePoint farm.' + } + } + } + } +} +``` + +Notes: + +- `Value` wins over `DefaultValue`. +- `Required`, `AllowedValues`, `MinLength`, `MaxLength`, and `Pattern` are validated by the resolver. +- `Sensitive` is currently metadata only. +- `Deprecated.Message` can be a string or a localized hashtable. +- Optional string parameters can allow empty values with a pattern like `'^$|^[A-Za-z][A-Za-z0-9_-]*$'`. + +## Variable Example + +Variables may reference parameters and other variables. Nested variable references are supported. Circular references are rejected. + +```powershell +@{ + Parameters = @{ + DatabasePrefix = @{ + Type = 'string' + DefaultValue = 'SharePoint' + } + DomainLabel = @{ + Type = 'string' + Value = 'LAN' + } + Landscape = @{ + Type = 'string' + Value = 'Test' + AllowedValues = @( + 'Prod', + 'Test' + ) + } + ServiceDatabaseSegment = @{ + Type = 'string' + DefaultValue = 'Services' + } + } + + Variables = @{ + StageCode = "[if(equals(parameters('Landscape'), 'Test'), 'TST', 'PRD')]" + DatabasePrefix = "[joinNotEmpty('_', parameters('DatabasePrefix'), parameters('DomainLabel'), variables('StageCode'))]" + ServiceDbPrefix = "[joinNotEmpty('_', variables('DatabasePrefix'), parameters('ServiceDatabaseSegment'))]" + ConfigDbName = "[joinNotEmpty('_', variables('DatabasePrefix'), 'Farm_Config')]" + } +} +``` + +Example output: + +```text +StageCode : TST +DatabasePrefix : SharePoint_LAN_TST +ServiceDbPrefix : SharePoint_LAN_TST_Services +ConfigDbName : SharePoint_LAN_TST_Farm_Config +``` + +## Functions + +### References + +```powershell +"[parameters('DatabasePrefix')]" +"[variables('ServiceDbPrefix')]" +``` + +`parameters(name)` returns the effective parameter value. `Value` is used before `DefaultValue`. + +`variables(name)` resolves another variable. Variables may reference other variables. + +### String Composition + +```powershell +"[concat('SharePoint', '_', 'Services')]" +# SharePoint_Services +``` + +```powershell +"[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Landscape'))]" +# SharePoint_LAN_Test +``` + +```powershell +"[joinNotEmpty('_', parameters('DatabasePrefix'), parameters('DomainLabel'), '', 'Services')]" +# SharePoint_LAN_Services +``` + +```powershell +"[defaultIfEmpty(parameters('DatabasePrefix'), 'SharePoint')]" +# SharePoint, when DatabasePrefix is empty +``` + +```powershell +"[coalesce(parameters('CustomPrefix'), parameters('DatabasePrefix'), 'SharePoint')]" +# First non-empty value +``` + +### Conditions And Boolean Logic + +```powershell +"[if(equals(parameters('Landscape'), 'Test'), 'TST', 'PRD')]" +# TST +``` + +```powershell +"[equals(parameters('Landscape'), 'Test')]" +"[notEquals(parameters('Landscape'), 'Prod')]" +"[and(equals(parameters('Landscape'), 'Test'), not(empty(parameters('DatabasePrefix'))))]" +"[or(equals(parameters('Landscape'), 'Prod'), equals(parameters('Landscape'), 'Test'))]" +"[not(empty(parameters('DatabasePrefix')))]" +``` + +### Case And Text + +```powershell +"[toLower('BGW-LAN')]" +# bgw-lan +``` + +```powershell +"[toUpper('bgw-lan')]" +# BGW-LAN +``` + +```powershell +"[trim(' SharePoint ')]" +"[trimStart(' SharePoint')]" +"[trimEnd('SharePoint ')]" +``` + +```powershell +"[replace('BGW-LAN', '-', '_')]" +# BGW_LAN +``` + +```powershell +"[substring('SharePoint', 5, 5)]" +# Point +``` + +```powershell +"[indexOf('BGW', 1)]" +# G +``` + +`firstIndexOf` and `lastIndexOf` return the first or last function argument: + +```powershell +"[firstIndexOf('a', 'b', 'c')]" +# a + +"[lastIndexOf('a', 'b', 'c')]" +# c +``` + +### Name Cleanup + +```powershell +"[sanitizeName(' SharePoint LAN/Test DB ')]" +# SharePoint_LAN_Test_DB +``` + +```powershell +"[sanitizeName(' SharePoint LAN/Test DB ', '-')]" +# SharePoint-LAN-Test-DB +``` + +```powershell +"[normalizeSeparator('__SharePoint___LAN_Test__', '_')]" +# SharePoint_LAN_Test +``` + +```powershell +"[prefixIfNotEmpty('LAN', 'BGW-')]" +# BGW-LAN +``` + +```powershell +"[suffixIfNotEmpty('SharePoint', '_DB')]" +# SharePoint_DB +``` + +### Collections + +```powershell +"[split(parameters('DomainFQDN'), '.')]" +# @('bgw-online', 'de') +``` + +```powershell +"[join(split(parameters('DomainFQDN'), '.'), '_')]" +# bgw-online_de +``` + +```powershell +"[first(split(parameters('DomainFQDN'), '.'))]" +# bgw-online +``` + +```powershell +"[last(split(parameters('DomainFQDN'), '.'))]" +# de +``` + +```powershell +"[take(split(parameters('DomainFQDN'), '.'), 1)]" +# @('bgw-online') +``` + +```powershell +"[skip(split(parameters('DomainFQDN'), '.'), 1)]" +# @('de') +``` + +```powershell +"[unique(split('SP.SP.SQL', '.'))]" +# @('SP', 'SQL') +``` + +```powershell +"[sort(split('SQL.SP.APP', '.'))]" +# @('APP', 'SP', 'SQL') +``` + +### Inspection + +```powershell +"[contains(parameters('DomainFQDN'), 'online')]" +# True +``` + +```powershell +"[contains(split(parameters('DomainFQDN'), '.'), 'de')]" +# True +``` + +```powershell +"[startsWith(parameters('DomainFQDN'), 'bgw')]" +# True +``` + +```powershell +"[endsWith(parameters('DomainFQDN'), 'de')]" +# True +``` + +```powershell +"[length(split(parameters('DomainFQDN'), '.'))]" +# 2 +``` + +```powershell +"[empty(parameters('OptionalValue'))]" +# True, when OptionalValue is empty +``` + +### Padding + +```powershell +"[padLeft('1', 2, '0')]" +# 01 +``` + +```powershell +"[padRight('SP', 4, '0')]" +# SP00 +``` + +## Full Function List + +- `parameters(name)` +- `variables(name)` +- `concat(value1, value2, ...)` +- `format(formatString, value1, value2, ...)` +- `coalesce(value1, value2, ...)` +- `defaultIfEmpty(value, defaultValue)` +- `if(condition, trueValue, falseValue)` +- `equals(left, right)` +- `notEquals(left, right)` +- `and(value1, value2, ...)` +- `or(value1, value2, ...)` +- `not(value)` +- `toLower(value)` +- `toUpper(value)` +- `firstIndexOf(value1, value2, ...)` +- `lastIndexOf(value1, value2, ...)` +- `indexOf(value, index)` +- `substring(value, startIndex)` +- `substring(value, startIndex, length)` +- `replace(value, oldValue, newValue)` +- `sanitizeName(value)` +- `sanitizeName(value, separator)` +- `normalizeSeparator(value, separator)` +- `prefixIfNotEmpty(value, prefix)` +- `suffixIfNotEmpty(value, suffix)` +- `contains(value, search)` +- `startsWith(value, search)` +- `endsWith(value, search)` +- `split(value, separator)` +- `join(array, separator)` +- `joinNotEmpty(separator, value1, value2, ...)` +- `take(array, count)` +- `skip(array, count)` +- `first(array)` +- `last(array)` +- `unique(array)` +- `sort(array)` +- `trim(value)` +- `trimStart(value)` +- `trimEnd(value)` +- `padLeft(value, totalWidth)` +- `padLeft(value, totalWidth, paddingCharacter)` +- `padRight(value, totalWidth)` +- `padRight(value, totalWidth, paddingCharacter)` +- `length(value)` +- `empty(value)` + +## Validation + +Run the tests: + +```powershell +Invoke-Pester -Script '.\PowerShell\Resolve-DSCConfigurationData\.tests\Resolve-DSCConfigurationData.Tests.ps1' +``` diff --git a/Resolve-DSCConfigurationData.psd1 b/Resolve-DSCConfigurationData.psd1 new file mode 100644 index 0000000..42ba29a --- /dev/null +++ b/Resolve-DSCConfigurationData.psd1 @@ -0,0 +1,24 @@ +@{ + RootModule = "Resolve-DSCConfigurationData.psm1" + ModuleVersion = "1.0.0" + GUID = "1d6ba0d4-93d5-4b0f-94c7-bf10a30fe0e8" + Author = "Torsten Brendgen" + Copyright = "(c) Torsten Brendgen. All rights reserved." + Description = "Resolves parameters, variables, and expressions in DSC configuration data." + PowerShellVersion = "5.1" + FunctionsToExport = @( + "Resolve-DSCConfigurationData" + ) + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @( + "DSC", + "ConfigurationData" + ) + ReleaseNotes = "Initial module layout." + } + } +} diff --git a/Resolve-DSCConfigurationData.psm1 b/Resolve-DSCConfigurationData.psm1 new file mode 100644 index 0000000..8c4eb93 --- /dev/null +++ b/Resolve-DSCConfigurationData.psm1 @@ -0,0 +1,13 @@ +$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath "Private" +$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath "Public" + +$Private = @(Get-ChildItem -Path $PrivatePath -Filter "*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName) +$Public = @(Get-ChildItem -Path $PublicPath -Filter "*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName) + +foreach($File in @($Private + $Public)){ + . $File.FullName +} + +Export-ModuleMember -Function @( + "Resolve-DSCConfigurationData" +)