diff --git a/.tests/Resolve-DSCConfigurationData.Tests.ps1 b/.tests/Resolve-DSCConfigurationData.Tests.ps1 index 4b886b3..7df514f 100644 --- a/.tests/Resolve-DSCConfigurationData.Tests.ps1 +++ b/.tests/Resolve-DSCConfigurationData.Tests.ps1 @@ -84,6 +84,54 @@ Describe "Resolve-DSCConfigurationData" { { 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" { $ConfigurationData = @{ Parameters = @{ @@ -124,6 +172,107 @@ Describe "Resolve-DSCConfigurationData" { { 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" { $ConfigurationData = @{ Parameters = @{ diff --git a/Private/Assert-ConfigurationDataParameter.ps1 b/Private/Assert-ConfigurationDataParameter.ps1 index 1b67847..d8ce651 100644 --- a/Private/Assert-ConfigurationDataParameter.ps1 +++ b/Private/Assert-ConfigurationDataParameter.ps1 @@ -42,6 +42,7 @@ function Assert-ConfigurationDataParameter { return } + Assert-ConfigurationDataParameterType -Name $Name -Definition $Definition -Value $Value Assert-ConfigurationDataParameterAllowedValue -Name $Name -Definition $Definition -Value $Value if(Test-ConfigurationDataMapContainsKey -Map $Definition -Key "MinLength"){ @@ -64,4 +65,20 @@ function Assert-ConfigurationDataParameter { 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]." + } + } } diff --git a/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 b/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 index 076bc0b..e2a98d8 100644 --- a/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 +++ b/Private/Assert-ConfigurationDataParameterAllowedValue.ps1 @@ -21,6 +21,16 @@ function Assert-ConfigurationDataParameterAllowedValue { 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){ throw "Parameter [$Name] value [$Value] is not allowed. Allowed values are: $($AllowedValues -join ', ')." } diff --git a/Private/Assert-ConfigurationDataParameterNumericValue.ps1 b/Private/Assert-ConfigurationDataParameterNumericValue.ps1 new file mode 100644 index 0000000..b5b3b3b --- /dev/null +++ b/Private/Assert-ConfigurationDataParameterNumericValue.ps1 @@ -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." + } +} diff --git a/Private/Assert-ConfigurationDataParameterType.ps1 b/Private/Assert-ConfigurationDataParameterType.ps1 new file mode 100644 index 0000000..7532956 --- /dev/null +++ b/Private/Assert-ConfigurationDataParameterType.ps1 @@ -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]." + } + } +} diff --git a/Private/Test-ConfigurationDataValueIsInteger.ps1 b/Private/Test-ConfigurationDataValueIsInteger.ps1 new file mode 100644 index 0000000..32ffc70 --- /dev/null +++ b/Private/Test-ConfigurationDataValueIsInteger.ps1 @@ -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]) +} diff --git a/Readme.md b/Readme.md index d9e8ecc..8868ed1 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# Resolve-DSCConfigurationData +# Resolve-DSCConfigurationData `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 MaxLength = 32 + # Optional numeric range validation. Applies to numeric values such as Type = 'int'. + MinValue = 1 + MaxValue = 65535 + # Optional regex validation. Pattern = '^[A-Za-z][A-Za-z0-9_-]*$' @@ -75,11 +79,72 @@ This block shows all currently supported parameter properties. Notes: - `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. - `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_-]*$'`. +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 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 = @{ Type = 'string' - Value = 'LAN' + Value = 'corp' } Landscape = @{ Type = 'string' @@ -122,9 +187,9 @@ Example output: ```text StageCode : TST -DatabasePrefix : SharePoint_LAN_TST -ServiceDbPrefix : SharePoint_LAN_TST_Services -ConfigDbName : SharePoint_LAN_TST_Farm_Config +DatabasePrefix : SharePoint_corp_TST +ServiceDbPrefix : SharePoint_corp_TST_Services +ConfigDbName : SharePoint_corp_TST_Farm_Config ``` ## Functions @@ -149,12 +214,12 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ```powershell "[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Landscape'))]" -# SharePoint_LAN_Test +# SharePoint_corp_Test ``` ```powershell "[joinNotEmpty('_', parameters('DatabasePrefix'), parameters('DomainLabel'), '', 'Services')]" -# SharePoint_LAN_Services +# SharePoint_corp_Services ``` ```powershell @@ -185,13 +250,13 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ### Case And Text ```powershell -"[toLower('BGW-LAN')]" -# bgw-lan +"[toLower('Contoso-CORP')]" +# contoso-corp ``` ```powershell -"[toUpper('bgw-lan')]" -# BGW-LAN +"[toUpper('contoso-corp')]" +# CONTOSO-CORP ``` ```powershell @@ -201,8 +266,8 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ``` ```powershell -"[replace('BGW-LAN', '-', '_')]" -# BGW_LAN +"[replace('Contoso-CORP', '-', '_')]" +# Contoso_CORP ``` ```powershell @@ -211,7 +276,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ``` ```powershell -"[indexOf('BGW', 1)]" +"[indexOf('CON', 1)]" # G ``` @@ -228,23 +293,23 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ### Name Cleanup ```powershell -"[sanitizeName(' SharePoint LAN/Test DB ')]" -# SharePoint_LAN_Test_DB +"[sanitizeName(' SharePoint corp/Test DB ')]" +# SharePoint_corp_Test_DB ``` ```powershell -"[sanitizeName(' SharePoint LAN/Test DB ', '-')]" -# SharePoint-LAN-Test-DB +"[sanitizeName(' SharePoint corp/Test DB ', '-')]" +# SharePoint-corp-Test-DB ``` ```powershell -"[normalizeSeparator('__SharePoint___LAN_Test__', '_')]" -# SharePoint_LAN_Test +"[normalizeSeparator('__SharePoint___corp_Test__', '_')]" +# SharePoint_corp_Test ``` ```powershell -"[prefixIfNotEmpty('LAN', 'BGW-')]" -# BGW-LAN +"[prefixIfNotEmpty('corp', 'Contoso-')]" +# Contoso-corp ``` ```powershell @@ -256,17 +321,17 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ```powershell "[split(parameters('DomainFQDN'), '.')]" -# @('bgw-online', 'de') +# @('contoso', 'com') ``` ```powershell "[join(split(parameters('DomainFQDN'), '.'), '_')]" -# bgw-online_de +# contoso_com ``` ```powershell "[first(split(parameters('DomainFQDN'), '.'))]" -# bgw-online +# contoso ``` ```powershell @@ -276,7 +341,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ```powershell "[take(split(parameters('DomainFQDN'), '.'), 1)]" -# @('bgw-online') +# @('contoso') ``` ```powershell @@ -307,7 +372,7 @@ ConfigDbName : SharePoint_LAN_TST_Farm_Config ``` ```powershell -"[startsWith(parameters('DomainFQDN'), 'bgw')]" +"[startsWith(parameters('DomainFQDN'), 'contoso')]" # True ```