Default Configurations for PS Modules

on Monday, November 25, 2019

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


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.