Add parameter validation functions and enhance tests for configuration data

This commit is contained in:
Torsten Brendgen
2026-06-27 14:22:37 +02:00
parent 3e017fc926
commit 086020c3df
7 changed files with 371 additions and 28 deletions

View File

@@ -84,6 +84,54 @@ Describe "Resolve-DSCConfigurationData" {
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
} }
It "validates array parameter items against AllowedValues" {
$ConfigurationData = @{
Parameters = @{
ServerRoles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"Application"
)
AllowedValues = @(
"WebFrontEnd",
"Application",
"Search"
)
}
}
Resources = @{
Roles = "[parameters('ServerRoles')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Roles[0] | Should Be "WebFrontEnd"
$Result.Resources.Roles[1] | Should Be "Application"
}
It "throws when an array parameter item is not listed in AllowedValues" {
$ConfigurationData = @{
Parameters = @{
ServerRoles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"InvalidRole"
)
AllowedValues = @(
"WebFrontEnd",
"Application",
"Search"
)
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates required parameters" { It "validates required parameters" {
$ConfigurationData = @{ $ConfigurationData = @{
Parameters = @{ Parameters = @{
@@ -124,6 +172,107 @@ Describe "Resolve-DSCConfigurationData" {
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw { Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
} }
It "validates supported parameter types" {
$ConfigurationData = @{
Parameters = @{
Name = @{
Type = "string"
Value = "SharePoint"
}
Port = @{
Type = "int"
Value = 1433
}
Enabled = @{
Type = "bool"
Value = $true
}
Roles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"Application"
)
}
SecretReference = @{
Type = "secureString"
Value = @{
SecretName = "SharePointFarmPassphrase"
}
Sensitive = $true
}
CredentialReference = @{
Type = "credential"
Value = @{
CredentialName = "SharePointSetup"
}
Sensitive = $true
}
}
Resources = @{
Name = "[parameters('Name')]"
Port = "[parameters('Port')]"
Enabled = "[parameters('Enabled')]"
RoleCount = "[length(parameters('Roles'))]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Name | Should Be "SharePoint"
$Result.Resources.Port | Should Be 1433
$Result.Resources.Enabled | Should Be $true
$Result.Resources.RoleCount | Should Be 2
}
It "throws when parameter type validation fails" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = "1433"
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates numeric ranges" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = 1433
MinValue = 1
MaxValue = 65535
}
}
Resources = @{
Port = "[parameters('Port')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Port | Should Be 1433
}
It "throws when numeric range validation fails" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = 70000
MinValue = 1
MaxValue = 65535
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "supports localized deprecated parameter messages" { It "supports localized deprecated parameter messages" {
$ConfigurationData = @{ $ConfigurationData = @{
Parameters = @{ Parameters = @{

View File

@@ -42,6 +42,7 @@ function Assert-ConfigurationDataParameter {
return return
} }
Assert-ConfigurationDataParameterType -Name $Name -Definition $Definition -Value $Value
Assert-ConfigurationDataParameterAllowedValue -Name $Name -Definition $Definition -Value $Value Assert-ConfigurationDataParameterAllowedValue -Name $Name -Definition $Definition -Value $Value
if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MinLength"){ if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MinLength"){
@@ -64,4 +65,20 @@ function Assert-ConfigurationDataParameter {
throw "Parameter [$Name] value [$Value] does not match pattern [$Pattern]." throw "Parameter [$Name] value [$Value] does not match pattern [$Pattern]."
} }
} }
if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MinValue"){
Assert-ConfigurationDataParameterNumericValue -Name $Name -Value $Value
$MinValue = [decimal](Get-ConfigurationDataMapValue -Map $Definition -Key "MinValue")
if(([decimal]$Value) -lt $MinValue){
throw "Parameter [$Name] value [$Value] is less than MinValue [$MinValue]."
}
}
if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MaxValue"){
Assert-ConfigurationDataParameterNumericValue -Name $Name -Value $Value
$MaxValue = [decimal](Get-ConfigurationDataMapValue -Map $Definition -Key "MaxValue")
if(([decimal]$Value) -gt $MaxValue){
throw "Parameter [$Name] value [$Value] is greater than MaxValue [$MaxValue]."
}
}
} }

View File

@@ -21,6 +21,16 @@ function Assert-ConfigurationDataParameterAllowedValue {
return return
} }
if($Value -is [System.Array] -and $Value -isnot [string]){
foreach($Item in @($Value)){
if($AllowedValues -notcontains $Item){
throw "Parameter [$Name] value [$Item] is not allowed. Allowed values are: $($AllowedValues -join ', ')."
}
}
return
}
if($AllowedValues -notcontains $Value){ if($AllowedValues -notcontains $Value){
throw "Parameter [$Name] value [$Value] is not allowed. Allowed values are: $($AllowedValues -join ', ')." throw "Parameter [$Name] value [$Value] is not allowed. Allowed values are: $($AllowedValues -join ', ')."
} }

View File

@@ -0,0 +1,21 @@
function Assert-ConfigurationDataParameterNumericValue {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Name,
[AllowNull()]
$Value
)
if($null -eq $Value -or $Value -is [bool] -or ($Value -isnot [ValueType])){
throw "Parameter [$Name] value [$Value] is not numeric."
}
try {
[void]([decimal]$Value)
} catch {
throw "Parameter [$Name] value [$Value] is not numeric."
}
}

View File

@@ -0,0 +1,61 @@
function Assert-ConfigurationDataParameterType {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Name,
[Parameter(Mandatory=$true)]
$Definition,
[AllowNull()]
$Value
)
if(-not (Test-ConfigurationDataMapContainsKey -Map $Definition -Key "Type")){
return
}
$TypeName = ([string](Get-ConfigurationDataMapValue -Map $Definition -Key "Type")).ToLowerInvariant()
switch($TypeName){
"string" {
if($Value -isnot [string]){
throw "Parameter [$Name] expects type [string], but received [$($Value.GetType().Name)]."
}
}
{ $_ -in @("int", "integer") } {
if(-not (Test-ConfigurationDataValueIsInteger -Value $Value)){
throw "Parameter [$Name] expects type [int], but received [$($Value.GetType().Name)]."
}
}
{ $_ -in @("bool", "boolean") } {
if($Value -isnot [bool]){
throw "Parameter [$Name] expects type [bool], but received [$($Value.GetType().Name)]."
}
}
"array" {
if($Value -isnot [System.Array] -or $Value -is [string]){
throw "Parameter [$Name] expects type [array], but received [$($Value.GetType().Name)]."
}
}
{ $_ -in @("hashtable", "object") } {
if(-not (Test-ConfigurationDataMap -Value $Value)){
throw "Parameter [$Name] expects type [hashtable], but received [$($Value.GetType().Name)]."
}
}
"securestring" {
if(($Value -isnot [string]) -and ($Value -isnot [System.Security.SecureString]) -and (-not (Test-ConfigurationDataMap -Value $Value))){
throw "Parameter [$Name] expects type [secureString], but received [$($Value.GetType().Name)]."
}
}
"credential" {
if(($Value -isnot [System.Management.Automation.PSCredential]) -and (-not (Test-ConfigurationDataMap -Value $Value))){
throw "Parameter [$Name] expects type [credential], but received [$($Value.GetType().Name)]."
}
}
default {
throw "Parameter [$Name] uses unsupported type [$TypeName]."
}
}
}

View File

@@ -0,0 +1,20 @@
function Test-ConfigurationDataValueIsInteger {
[CmdletBinding()]
Param(
[AllowNull()]
$Value
)
if($null -eq $Value -or $Value -is [bool]){
return $false
}
return ($Value -is [byte]) -or
($Value -is [sbyte]) -or
($Value -is [int16]) -or
($Value -is [uint16]) -or
($Value -is [int]) -or
($Value -is [uint32]) -or
($Value -is [long]) -or
($Value -is [uint64])
}

121
Readme.md
View File

@@ -1,4 +1,4 @@
# Resolve-DSCConfigurationData # Resolve-DSCConfigurationData
`Resolve-DSCConfigurationData` resolves parameter, variable, and expression references in DSC configuration data. `Resolve-DSCConfigurationData` resolves parameter, variable, and expression references in DSC configuration data.
@@ -47,6 +47,10 @@ This block shows all currently supported parameter properties.
MinLength = 2 MinLength = 2
MaxLength = 32 MaxLength = 32
# Optional numeric range validation. Applies to numeric values such as Type = 'int'.
MinValue = 1
MaxValue = 65535
# Optional regex validation. # Optional regex validation.
Pattern = '^[A-Za-z][A-Za-z0-9_-]*$' Pattern = '^[A-Za-z][A-Za-z0-9_-]*$'
@@ -75,11 +79,72 @@ This block shows all currently supported parameter properties.
Notes: Notes:
- `Value` wins over `DefaultValue`. - `Value` wins over `DefaultValue`.
- `Required`, `AllowedValues`, `MinLength`, `MaxLength`, and `Pattern` are validated by the resolver. - `Type`, `Required`, `AllowedValues`, `MinLength`, `MaxLength`, `MinValue`, `MaxValue`, and `Pattern` are validated by the resolver.
- `AllowedValues` validates scalar values directly and array values item by item.
- `Sensitive` is currently metadata only. - `Sensitive` is currently metadata only.
- `Deprecated.Message` can be a string or a localized hashtable. - `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_-]*$'`. - Optional string parameters can allow empty values with a pattern like `'^$|^[A-Za-z][A-Za-z0-9_-]*$'`.
Supported parameter types:
- `string`
- `int` / `integer`
- `bool` / `boolean`
- `array`
- `hashtable` / `object`
- `secureString`
- `credential`
`secureString` and `credential` are intended for secret references, not raw secrets in PSD1 files:
```powershell
FarmPassphrase = @{
Type = 'secureString'
Required = $true
Sensitive = $true
Value = @{
SecretName = 'SharePointFarmPassphrase'
}
}
SetupCredential = @{
Type = 'credential'
Required = $true
Sensitive = $true
Value = @{
CredentialName = 'SharePointSetup'
}
}
```
Array values can be restricted item by item:
```powershell
ServerRoles = @{
Type = 'array'
Value = @(
'WebFrontEnd',
'Application'
)
AllowedValues = @(
'WebFrontEnd',
'Application',
'Search'
)
}
```
Numeric values can be restricted with `MinValue` and `MaxValue`:
```powershell
SqlPort = @{
Type = 'int'
DefaultValue = 1433
MinValue = 1
MaxValue = 65535
}
```
## Variable Example ## Variable Example
Variables may reference parameters and other variables. Nested variable references are supported. Circular references are rejected. Variables may reference parameters and other variables. Nested variable references are supported. Circular references are rejected.
@@ -93,7 +158,7 @@ Variables may reference parameters and other variables. Nested variable referenc
} }
DomainLabel = @{ DomainLabel = @{
Type = 'string' Type = 'string'
Value = 'LAN' Value = 'corp'
} }
Landscape = @{ Landscape = @{
Type = 'string' Type = 'string'
@@ -122,9 +187,9 @@ Example output:
```text ```text
StageCode : TST StageCode : TST
DatabasePrefix : SharePoint_LAN_TST DatabasePrefix : SharePoint_corp_TST
ServiceDbPrefix : SharePoint_LAN_TST_Services ServiceDbPrefix : SharePoint_corp_TST_Services
ConfigDbName : SharePoint_LAN_TST_Farm_Config ConfigDbName : SharePoint_corp_TST_Farm_Config
``` ```
## Functions ## Functions
@@ -149,12 +214,12 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
```powershell ```powershell
"[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Landscape'))]" "[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Landscape'))]"
# SharePoint_LAN_Test # SharePoint_corp_Test
``` ```
```powershell ```powershell
"[joinNotEmpty('_', parameters('DatabasePrefix'), parameters('DomainLabel'), '', 'Services')]" "[joinNotEmpty('_', parameters('DatabasePrefix'), parameters('DomainLabel'), '', 'Services')]"
# SharePoint_LAN_Services # SharePoint_corp_Services
``` ```
```powershell ```powershell
@@ -185,13 +250,13 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
### Case And Text ### Case And Text
```powershell ```powershell
"[toLower('BGW-LAN')]" "[toLower('Contoso-CORP')]"
# bgw-lan # contoso-corp
``` ```
```powershell ```powershell
"[toUpper('bgw-lan')]" "[toUpper('contoso-corp')]"
# BGW-LAN # CONTOSO-CORP
``` ```
```powershell ```powershell
@@ -201,8 +266,8 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
``` ```
```powershell ```powershell
"[replace('BGW-LAN', '-', '_')]" "[replace('Contoso-CORP', '-', '_')]"
# BGW_LAN # Contoso_CORP
``` ```
```powershell ```powershell
@@ -211,7 +276,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
``` ```
```powershell ```powershell
"[indexOf('BGW', 1)]" "[indexOf('CON', 1)]"
# G # G
``` ```
@@ -228,23 +293,23 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
### Name Cleanup ### Name Cleanup
```powershell ```powershell
"[sanitizeName(' SharePoint LAN/Test DB ')]" "[sanitizeName(' SharePoint corp/Test DB ')]"
# SharePoint_LAN_Test_DB # SharePoint_corp_Test_DB
``` ```
```powershell ```powershell
"[sanitizeName(' SharePoint LAN/Test DB ', '-')]" "[sanitizeName(' SharePoint corp/Test DB ', '-')]"
# SharePoint-LAN-Test-DB # SharePoint-corp-Test-DB
``` ```
```powershell ```powershell
"[normalizeSeparator('__SharePoint___LAN_Test__', '_')]" "[normalizeSeparator('__SharePoint___corp_Test__', '_')]"
# SharePoint_LAN_Test # SharePoint_corp_Test
``` ```
```powershell ```powershell
"[prefixIfNotEmpty('LAN', 'BGW-')]" "[prefixIfNotEmpty('corp', 'Contoso-')]"
# BGW-LAN # Contoso-corp
``` ```
```powershell ```powershell
@@ -256,17 +321,17 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
```powershell ```powershell
"[split(parameters('DomainFQDN'), '.')]" "[split(parameters('DomainFQDN'), '.')]"
# @('bgw-online', 'de') # @('contoso', 'com')
``` ```
```powershell ```powershell
"[join(split(parameters('DomainFQDN'), '.'), '_')]" "[join(split(parameters('DomainFQDN'), '.'), '_')]"
# bgw-online_de # contoso_com
``` ```
```powershell ```powershell
"[first(split(parameters('DomainFQDN'), '.'))]" "[first(split(parameters('DomainFQDN'), '.'))]"
# bgw-online # contoso
``` ```
```powershell ```powershell
@@ -276,7 +341,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
```powershell ```powershell
"[take(split(parameters('DomainFQDN'), '.'), 1)]" "[take(split(parameters('DomainFQDN'), '.'), 1)]"
# @('bgw-online') # @('contoso')
``` ```
```powershell ```powershell
@@ -307,7 +372,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config
``` ```
```powershell ```powershell
"[startsWith(parameters('DomainFQDN'), 'bgw')]" "[startsWith(parameters('DomainFQDN'), 'contoso')]"
# True # True
``` ```