Private/deploy/Configuration/Resolve-ScriptedToken.ps1

<#
The MIT License (MIT)
 
Copyright (c) 2015 Objectivity Bespoke Software Specialists
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#>


function Resolve-ScriptedToken {
    <#
    .SYNOPSIS
    Resolves token if it is provided as ScriptBlock.
     
    .DESCRIPTION
    Works for $Tokens and $Outputs scripts. The $Outputs is resolved only when passed hashtable contains
    specified value, otherwise ScriptBlock is not resolved and returned as provided..
 
    .PARAMETER ScriptedToken
    Object to resolve if it is a ScriptBlock
 
    .PARAMETER ResolvedTokens
    Hashtable containing resolved tokens - will be available as $Tokens variable inside the scriptblock.
 
    .PARAMETER TokenName
    Token name (only for logging purposes).
 
    .PARAMETER TokenCategory
    Name of category the parsed token belongs to (only for logging purposes).
 
    .PARAMETER Node
    Value of $Node variable that will be available inside the scriptblock.
 
    .PARAMETER Environment
    Value of $Environment variable that will be available inside the scriptblock.
 
    .PARAMETER Outputs
    Value of $Outputs variable that will be availabe inside the scriptblock.
 
    .EXAMPLE
        $credentials = Resolve-ScriptedToken {$Tokens.General.Credentials}
 
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory=$false)]
        [AllowNull()]
        [object]
        $ScriptedToken,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $ResolvedTokens,

        [Parameter(Mandatory=$false)]
        [string]
        $TokenName,

        [Parameter(Mandatory=$false)]
        [string] 
        $TokenCategory,

        [Parameter(Mandatory=$false)]
        [string]
        $Node,

        [Parameter(Mandatory=$true)]
        [string]
        $Environment,

        [Parameter(Mandatory=$false)]
        [hashtable]
        $Outputs
    )

    # Add 'NodeName' and 'Tokens' variables
    $contextVariables = @(
        (New-Object -TypeName System.Management.Automation.PSVariable -ArgumentList 'NodeName', $Node),
        (New-Object -TypeName System.Management.Automation.PSVariable -ArgumentList 'Node', $Node),
        (New-Object -TypeName System.Management.Automation.PSVariable -ArgumentList 'Tokens', $ResolvedTokens)
        (New-Object -TypeName System.Management.Automation.PSVariable -ArgumentList 'Outputs', $Outputs)
        (New-Object -TypeName System.Management.Automation.PSVariable -ArgumentList 'Environment', $Environment)
    )

    $i = 0

    $tokensRegexMatch = '(\$Tokens|\$Outputs)\.(\w+)\.(\w+)'
    while ($ScriptedToken -is [ScriptBlock] -and $i -lt 20) {
        if ($ScriptedToken.ToString() -imatch $tokensRegexMatch) {
            $itemType = $Matches[1]
            $refTokenCategory = $Matches[2]
            $refTokenName = $Matches[3]
            $refTokenFullName = "$refTokenCategory.$refTokenName"
            if ($itemType -eq '$Tokens') {
                if (!$ResolvedTokens.ContainsKey($refTokenCategory) -or !$ResolvedTokens[$refTokenCategory].ContainsKey($refTokenName)) {
                    Write-MissingToken -ScriptedToken $ScriptedToken -TokenName $TokenName -TokenCategory $TokenCategory -Environment $Environment -ItemType $itemType -MissingTokenName $refTokenFullName
                }
            } elseif ($itemType -eq '$Outputs') {
                if (!$Outputs -or !$Outputs.ContainsKey($refTokenCategory) -or !$Outputs[$refTokenCategory].ContainsKey($refTokenName)) {
                    Write-MissingToken -ScriptedToken $ScriptedToken -TokenName $TokenName -TokenCategory $TokenCategory -Environment $Environment -ItemType $itemType -MissingTokenName $refTokenFullName
                    break
                }
            }
        }        

        $ScriptedToken = $ScriptedToken.InvokeWithContext($null, $contextVariables, $null)
        # InvokeWithContext always returns a collection, but there are following cases:
        # - if scriptblock returns another scriptblock, we need to get it from first element of the array
        # - if scriptblock returns array, we need to get it as it is (it doesn't return collection of collections but just a collection)
        # - if scriptblock returns any other value, we can get it as it is (PS will automatically unbox it from one-element collection)
        if ($ScriptedToken[0] -is [ScriptBlock]) {
            $ScriptedToken = $ScriptedToken[0]
        }
        $i++
    }
    if ($i -eq 20) {
        throw 'Too many nested script tokens (more than 20 loops). Ensure you don''t have circular reference in your tokens (e.g. a={ $ResolvedTokens.b }, b={ $ResolvedTokens.a })'
    }

    return $ScriptedToken
}

function Write-MissingToken {
    <#
    .SYNOPSIS
    Helper functions, logs the warn message for missing tokens.
 
    .PARAMETER ScriptedToken
    Object to resolve if it is a ScriptBlock
 
    .PARAMETER TokenName
    Token name (only for logging purposes).
 
    .PARAMETER TokenCategory
    Name of category the parsed token belongs to (only for logging purposes).
 
    .PARAMETER Environment
    The name of the environment.
 
    .PARAMETER ItemType
    The type of the missing token ($Tokens or $Outputs)
 
    .PARAMETER MissingTokenName
    The name of the missing token, concatenated category and name.
 
    .EXAMPLE
    Write-MissingToken $params
 
    #>

    
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [object]
        $ScriptedToken,

        [Parameter(Mandatory=$false)]
        [string]
        $TokenName,

        [Parameter(Mandatory=$false)]
        [string] 
        $TokenCategory,

        [Parameter(Mandatory=$true)]
        [string]
        $Environment,

        [Parameter(Mandatory=$true)]
        [string]
        $ItemType,

        [Parameter(Mandatory=$true)]
        [string]
        $MissingTokenName

    )

    if ($TokenCategory) {
        $tokenLog = "$TokenCategory.$TokenName"
    } else {
        $tokenLog = $TokenName
    }
    
    if ($Global:MissingScriptBlockTokens -and !$Global:MissingScriptBlockTokens.ContainsKey($missingTokenName)) { 
        # This is to prevent logging the same warning multiple times (tokens are resolved for each deployment plan step)
        $Global:MissingScriptBlockTokens[$missingTokenName] = $true
        Write-Log -Warn "Cannot resolve '$ItemType.$MissingTokenName' in token '$tokenLog' = '{$($ScriptedToken.ToString())}' / Environment '$Environment'."
    }
}