A common problem with Powershell modules is that they need to be configured slightly differently when being used for different needs. For example, developers may want a module to use a local instance of a service in order to do development or testing. But, on a server, the module might be expected to connect to the instance of a service specific for that environment. These are two separate groups of users, but each has the same need, a default configuration that makes sense for them.
One way we’ve found to help make this a little more manageable is to create a standardized way to configure local default configuration’s for developers, while creating an interface which can be used by service providers to set default configurations for use on the servers.
This comes about by standardizing on 4 functions:
- Set-{ModuleName}Config –Environment [Prod|Test|Dev|Local]
This is the function that most people will use. If you want to point that module to use a particular environments services, use this function.
For developers, this is useful to point the module at their most commonly used environment. For a service they help build and maintain, that would most likely be local. But, for service they only consume, that is usually Prod.
For module developers, this function can be used to set the default configuration for the module. In general, this turns out to be defaulted to Prod. If your not the developer of a service, and you are going to use a Powershell module to interact with that service, you’re generally wanting to point it to Prod. This is the most common use case, and module developers usually setup module defaults for the most common use case.
For service developers that use the module within their services, this command is flexible enough for them to determine what environment their service is running in and set up the module to connect to the correct endpoints. - Save-{ModuleName}DefaultConfig
This is mostly used by developers.
Once you have the environment setup the way you want it, use the Save function to save the configuration locally to disk. We have had success saving this file under the users local folder (right next to their profile); so the settings are not machine wide, but user specific.
- Restore-{ModuleName}DefaultConfig
This function usually isn’t called by developers / end users.
This function is called when the module loads and it will check if the user has a local configuration file. If it finds one, it will load the values into memory.
Services usually don’t have a local configuration file. - Test-{ModuleName}Configured
This function usually won't be called by the end user. It's used internally to determine if all the important properties are setup before saving the properties to disk.
To get people to adopt this strategy, you have to make it easy for module developers to add the functionality into their module. To do that there’s one more function:
- Add-DefaultConfigToModule –ModuleName <ModuleName> –Path <Path>
This will add 4 templated files to a module, one for each function. It will also update the .psm1 file to end with a call to Restore-{ModuleName}DefaultConfig.
Below is a very mashed together version of the files for the module.
The code does assume all the module configuration information is stored in $global:ModuleName
# http://stackoverflow.com/questions/1183183/path-of-currently-executing-powershell-script | |
$root = Split-Path $MyInvocation.MyCommand.Path -Parent; | |
function Add-DefaultConfigToModule { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName, | |
[string] $Path = "." | |
) | |
# Validation | |
if(-not [System.IO.Directory]::Exists($Path)) { | |
throw "Could not find '$Path'. Please ensure module $ModuleName exists at the path and try again." | |
} | |
$dirInfo = New-Object System.IO.DirectoryInfo $Path | |
$Path = $dirInfo.FullName # just setting the path to the full name before usage | |
if($dirInfo.Name -ne $ModuleName) { | |
throw "Module $ModuleName does not exist at '$Path'. Please ensure the Path points to the module on disk and try again." | |
} | |
$psm1Path = Join-Path -Path $dirInfo.FullName -ChildPath "$ModuleName.psm1" | |
if(-not [System.IO.File]::Exists($psm1Path)) { | |
throw "Module $ModuleName's .psm1 file could not be found at '$Path'. Please ensure Path points to a module on disk and try again." | |
} | |
# Processing | |
$pubPath = Join-Path -Path $Path -ChildPath "public" | |
if((Test-Path -Path $pubPath) -eq $false) { | |
New-Item -Path $pubPath -ItemType Directory | Out-Null | |
} | |
$modulePath = Split-Path -Path $MyInvocation.MyCommand.Module.Path -Parent | |
$resourcePath = Join-Path -Path $modulePath -ChildPath "resources" | |
$addTemplatePath = Join-Path -Path $resourcePath -ChildPath "AddTemplate" | |
$files = Get-ChildItem -Path $addTemplatePath -File | |
# Adding template files | |
foreach($f in $files) { | |
# copy template file to module | |
$name = $f.Name | |
$destPath = Join-Path -Path $pubPath -ChildPath $f.Name | |
if((Test-Path -Path $destPath) -eq $true) { | |
Write-Host "Skipping $name (Already Exists)" | |
continue | |
} | |
Copy-Item -Path $f.FullName -Destination $destPath | |
# rename file | |
$rename = $name -replace "RModuleName", $ModuleName | |
$renamePath = Join-Path -Path $pubPath -ChildPath $rename | |
Move-Item -Path $destPath -Destination $renamePath | |
# replace occurrences of RModuleName with the real $ModuleName | |
$c = Get-Content -Path $renamePath | |
$c = $c |% { $_ -replace "RModuleName", $ModuleName } | |
$c | Set-Content -Path $renamePath -Force | |
Write-Host "Added $rename" | |
} | |
# Add to .psm1 file | |
$restoreCmd = "Restore-$($ModuleName)DefaultConfig" | |
$c = Get-Content -Path $psm1Path | |
$foundRestoreCmd = $c |? { $_ -match $restoreCmd } | |
if($foundRestoreCmd.Count -eq 0) { | |
$c += @("",$restoreCmd) | |
$c | Set-Content -Path $psm1Path -Force | |
Write-Host "Added '$restoreCmd' to $ModuleName.psm1" | |
} | |
} | |
function Get-ModuleDefaultConfigFilePath { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName | |
) | |
# Get the profile location for the current user in any host (console, ISE, VSCode, etc.) | |
$profileDir = $profile.CurrentUserAllHosts | Split-Path -Parent | |
return "$profileDir\$($ModuleName)DefaultConfig.json" | |
} | |
function Get-ModuleConfig { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName | |
) | |
$config = Get-Variable -Scope global -Name $ModuleName -ErrorAction SilentlyContinue | |
if($null -eq $config) { | |
throw "Global variable, `$global:$ModuleName, could not be found. Please ensure it exists and try again." | |
} | |
$config = $config.Value # Get-Variable sends back an object, use $object.value to get the value | |
return $config | |
} | |
function Restore-ModuleDefaultConfig { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName | |
) | |
$defaultConfigFile = Get-ModuleDefaultConfigFilePath -ModuleName $ModuleName | |
if (Test-Path $defaultConfigFile) { | |
$json = Get-Content $defaultConfigFile -Raw | |
$defaultConfig = ConvertFrom-Json $json | |
$properties = $defaultConfig.PSObject.Properties | |
if($null -ne $properties) { | |
$config = Get-Variable -Scope global -Name $ModuleName | |
$config = $config.Value | |
if($null -eq $config) { | |
$config = @{} | |
Set-Variable -Scope global -Name $ModuleName -Value $config | Out-Null | |
} | |
foreach($p in $properties) { | |
$config[$p.Name] = $p.Value | |
} | |
} | |
} | |
} | |
function Save-ModuleDefaultConfig { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName, | |
[string[]] $Properties = @() | |
) | |
Test-ModuleDefaultConfigured -ModuleName $ModuleName | Out-Null | |
$defaultConfigFile = Get-ModuleDefaultConfigFilePath -ModuleName $ModuleName | |
$Properties = Set-PropertiesList -ModuleName $ModuleName -Properties $Properties | |
$config = Get-ModuleConfig -ModuleName $ModuleName | |
$defaultConfig = @{} | |
foreach($p in $Properties) { | |
$defaultConfig[$p] = $config[$p] | |
} | |
$json = ConvertTo-Json -InputObject $defaultConfig # should only be one level deep | |
$json | Set-Content -Path $defaultConfigFile | |
Write-Host "Saved Config to $defaultConfigFile" | |
} | |
function Set-PropertiesList { | |
param( | |
[string] $ModuleName, | |
[string[]] $Properties = @() | |
) | |
if($Properties.Count -eq 0) { | |
$config = Get-ModuleConfig -ModuleName $ModuleName | |
foreach($k in $config.Keys) {$Properties += @($k)} | |
} | |
return $Properties | |
} | |
function Test-ModuleDefaultConfigured { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory = $true)] | |
[string] $ModuleName, | |
[string[]] $Properties = @() | |
) | |
$Properties = Set-PropertiesList -ModuleName $ModuleName -Properties $Properties | |
$config = Get-ModuleConfig -ModuleName $ModuleName | |
foreach($p in $Properties) { | |
$value = $config[$p] | |
if([string]::IsNullOrWhiteSpace($value)) { | |
throw @( | |
"`$global:$ModuleName does not have property '$p' configured. ", | |
"Please use Set-$($ModuleName)Config to set the needed values and try again." | |
) -join "" | |
} | |
} | |
return $true | |
} |
And, these files are to be placed within a subdirectory of the DefaultConfig module called /resources/AddTemplate:
function Restore-RModuleNameDefaultConfig { | |
Import-Module DefaultConfig | |
DefaultConfig\Restore-ModuleDefaultConfig -ModuleName "RModuleName" | |
} |
function Save-RModuleNameDefaultConfig { | |
Import-Module DefaultConfig | |
# basic example - this will save all properties in $global:RModuleName | |
DefaultConfig\Save-ModuleDefaultConfig -ModuleName "RModuleName" | |
<# | |
# normal example - this will save a subset of properties from $global:RModuleName | |
DefaultConfig\Save-ModuleDefaultConfig ` | |
-ModuleName "RModuleName" ` | |
-Properties @("Api", "DefaultUser") | |
#> | |
} |
function Set-RModuleNameConfig { | |
param( | |
[ValidateSet("Prod","Test","Dev","Local")] | |
[string] $Environment = "Prod" | |
) | |
<# | |
Example of what could be done in the set command | |
switch($Environment) { | |
"Prod" { | |
$global:RModuleName.Api = "http://some.site.com/api/" | |
} | |
"Test" { | |
$global:RModuleName.Api = "http://some.test.site.com/api/" | |
} | |
"Dev" { | |
$global:RModuleName.Api = "http://some.dev.site.com/api/" | |
} | |
"Local" { | |
$global:RModuleName.Api = "http://some.local.site.com/api/" | |
} | |
} | |
#> | |
} |
function Test-RModuleNameConfigured { | |
Import-Module DefaultConfig | |
DefaultConfig\Test-ModuleDefaultConfigured -ModuleName "RModuleName" | |
} |
0 comments:
Post a Comment