TD.Util.psm1

<#
.SYNOPSIS
Get Azure Keyvault secrets and add them to token collection
 
.DESCRIPTION
Get secrets from Azure Keyvault and add them to token collection
 
.PARAMETER Vault
Name of the Azure KeyVault
 
.PARAMETER Tokens
Hashtable to add secrets to
 
.PARAMETER SubscriptionId
Azure Subscription ID
 
.PARAMETER ServicePrincipal
Azure ServicePrincipal ID
 
.Example
$Tokens = @{}
Add-TokensFromAzureKeyVault -Vault 'MyVaultName' -Tokens $Tokens -SubscriptionId 'mySubscriptionId'
#>

function Add-TokensFromAzureKeyVault($Vault, $Tokens, $SubscriptionId, $ServicePrincipal)
{
    function Add-Secret($Name, $Value)
    {
        if (!$Tokens.ContainsKey($Name))
        {
            Write-Host "Adding secret $Name : *******"
            $Tokens.Add($Name, $Value)
        }
    }

    if ($null -eq (Get-Module -ListAvailable 'Az'))
    {
        Install-Module -Name Az -AllowClobber -Scope CurrentUser -Repository PSGallery -Force
        Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser -Repository PSGallery -Force
    }
    else
    {
        Import-Module Az -Scope local -Force
        Import-Module Az.Accounts -Scope local -Force
    }

    if (!!$env:SYSTEM_TEAMPROJECT)
    {
        $token = $(az account get-access-token --query accessToken --output tsv)
        $id = $(az account show --query user.name --output tsv)
        Connect-AzAccount -AccessToken $token -AccountId $id -Scope Process
    }
    else
    {
        if ($ServicePrincipal )
        {
            Connect-AzAccount -ServicePrincipal $ServicePrincipal
        }

        if ($SubscriptionId)
        {
            $ctxList = Get-AzContext -ListAvailable
            foreach ($ctx in $ctxList)
            {
                if ($ctx.Subscription.Id -eq $SubscriptionId)
                {
                    Select-AzContext -Name $ctx.Name
                    break
                }
            }
        }
    }

    $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
    if (-not $azProfile.Accounts.Count)
    {
        Throw "Powershell Az error: Ensure you are logged in."
    }

    $warning = (Get-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings -ErrorAction Ignore) -eq 'true'
    Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true"    
    try 
    {
        $secrets = Get-AzKeyVaultSecret -VaultName $Vault
        foreach ($secret in $secrets)
        {
            $s = Get-AzKeyVaultSecret -VaultName $Vault -Name $secret.Name
            #$pass = $s.SecretValue | ConvertFrom-SecureString -AsPlainText
            $cred = New-Object System.Management.Automation.PSCredential($secret.Name, $s.SecretValue)
            Add-Secret $secret.Name $cred
        }       
    }
    finally 
    {
        Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings $warning
    }
}
<#
.SYNOPSIS
Get tokens from config repository and add them to token collection
 
.DESCRIPTION
Get tokens from xml config repository and add them to token collection
 
.PARAMETER ConfigPath
Root path of the xml config files
 
.PARAMETER Tokens
Hashtable to add tokens to
 
.PARAMETER Env
Token environment filter, filter the tokens by environent like local, develop, test etc...
 
.Example
$Tokens = @{}
Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local'
#>

function Add-TokensFromConfig($ConfigPath, $Tokens, $Env)
{
    function Add-Var($Nodes, $NameProp = 'name', $ValueProp = 'value', $Prefix)
    {
        foreach ($node in $Nodes)
        {
            $name = $node."$NameProp"
            $value = $node."$ValueProp"
            $pre = $Prefix

            if ($node.LocalName -eq 'node')
            {
                if ($node.ParentNode.ParentNode.name -ne $Env)
                {
                    continue
                }
            }
            elseif ($node.LocalName -eq 'system-user')
            {
                if ($node.ParentNode.LocalName -eq 'application')
                {
                    $pre = "$Prefix$($node.ParentNode.name)"
                }
            }

            if ($pre)
            {
                $kn = "$pre$name"
                Write-Host "Adding variable $kn : $value"
                if (!$Tokens.ContainsKey($kn))
                {
                    $Tokens.Add($kn, $value)
                }
            }
            else
            {
                if (!$Tokens.ContainsKey($name))
                {
                    Write-Host "Adding variable $name : $value"
                    $Tokens.Add($name, $value)
                }
            }
        }
    }

    Get-ChildItem "$ConfigPath\*.xml" -Recurse | ForEach-Object {
        $doc = [xml] (Get-Content $_.Fullname)
        $nodes = $doc.SelectNodes("//variable[@environment='$Env' or not(@environment)]")
        Add-Var $nodes
        $nodes = $doc.SelectNodes("//node")
        if ($nodes.Count -gt 0)
        {
            Add-Var $nodes -NameProp 'role' -ValueProp 'name' -Prefix 'node-'
        }
        $nodes = $doc.SelectNodes("//service[@environment='$Env' or not(@environment)]")
        if ($nodes.Count -gt 0)
        {
            Add-Var $nodes -Prefix 'service-'
        }
        $nodes = $doc.SelectNodes("//system-user[@environment='$Env' or not(@environment)]")
        if ($nodes.Count -gt 0)
        {
            Add-Var $nodes -NameProp 'system-user' -ValueProp 'name' -Prefix 'system-user-'
        }
        $envNode = $doc.SelectSingleNode("//environment[@name='$Env']")
        if ($envNode)
        {
            $Tokens.Add('env-name', $envNode.'name')
            $Tokens.Add('env-group', $envNode.'group')
            $Tokens.Add('env-name-short', $envNode.'name-short')
            $Tokens.Add('env-name-suffix', $envNode.'name-suffix')
            $Tokens.Add('env-type', $envNode.'type')
            $Tokens.Add('env-active', $envNode.'active')
            $Tokens.Add('env-domain', $envNode.'domain')
            $Tokens.Add('env-domain-full', $envNode.'domain-full')
            $Tokens.Add('env-domain-description', $envNode.'description')
            $Tokens.Add('env-domain-owner', $envNode.'owner')
            $Tokens.Add('env-domain-notes', $envNode.'notes')
            $Tokens.Add('env-ps-remote-user', $envNode.'ps-remote-user')
            $Tokens.Add('env-subscription-id', $envNode.'subscription-id')
            $Tokens.Add('env-vault', $envNode.'vault')
        }
    }
}
<#
.SYNOPSIS
Convert the tokens in file to their actual values
 
.DESCRIPTION
Convert the tokens in file to their actual values
 
.PARAMETER FileName
Name of the file to convert
 
.PARAMETER PrefixToken
Token prefix
 
.PARAMETER SuffixToken
Token suffix
 
.PARAMETER ShowTokensUsed
Switch to echo tokens replaced
 
.PARAMETER SecondPass
Switch to signal that same file is used in multiple conversions
 
.PARAMETER Tokens
Hashtable to add tokens to
 
.Example
$Tokens = @{}
Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local'
 
Get-ChildItem .\$ConfigLocation\*.* | ForEach-Object {
    $destFile = Join-Path $ArtifactsLocation $_.Name
    Convert-TokensInFile -FileName $_.Fullname -DestFileName $destFile -Tokens $Tokens
}
 
#>

function Convert-TokensInFile($FileName, $PrefixToken = '__', $SuffixToken = '__', $DestFileName, [Switch]$ShowTokensUsed, [Switch]$SecondPass, $Tokens)
{
    if (!$DestFileName) { $DestFileName = $FileName }

    if (Test-Path $FileName)
    {
        $regex = [regex] "${PrefixToken}((?:(?!${SuffixToken}).)*)${SuffixToken}"
        $content = [System.IO.File]::ReadAllText($FileName);
        if (!$Tokens) 
        {
            $Tokens = @{}
        }
        $script:cnt = 0
        $callback = {
            param([System.Text.RegularExpressions.Match] $Match)
            $value = $Match.Groups[1].Value

            # check env first
            $newTokenValue = [Environment]::GetEnvironmentVariable($value)
            if ($null -eq $newTokenValue)
            {
                if ($Tokens.ContainsKey($value))
                {
                    $newTokenValue = $Tokens[$value]

                    # detect expression in variable
                    if ($newTokenValue.ToString().StartsWith('$'))
                    {
                        $newTokenValue = Invoke-Expression "Write-Output `"$($newTokenValue)`""
                    }                    
                }
            }
            if ($null -eq $newTokenValue)
            {
                $script:HasReplaceVarErrors = $true;
                Write-Warning "Token not found in replace: '$value'"
                return ""
            }

            $script:cnt++
            if ($ShowTokensUsed.IsPresent -or ($Global:VerbosePreference -eq 'Continue'))
            {
                Write-Host "Replacing token '$value' with '$newTokenValue'"
            }
            return $newTokenValue
        }

        $content = $regex.Replace($content, $callback)

        New-Item -ItemType Directory (Split-Path -Path $DestFileName) -Force -ErrorAction Ignore | Out-Null
        Set-Content -Path $DestFileName -Value $content -Encoding UTF8

        if ($Global:VerbosePreference -eq 'Continue')
        {
            if ($SecondPass.IsPresent -and ($script:cnt -eq 0) )
            {
                #ignore
            }
            else
            {
                LogInfo "Tokens replaced: $($script:cnt)"
            }
        }

    }
    else
    {
        Throw "Convert-TokensInFile error file not found '$FileName'"
    }
}
<#
.SYNOPSIS
Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store
 
.DESCRIPTION
Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local.
 
.PARAMETER Url
Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany)
 
.Example
$token = Get-AzureDevOpsAccessToken 'https://mycompany@dev.azure.com/mycompany')
#>

function Get-AzureDevOpsAccessToken($Url)
{
    $token = $env:SYSTEM_ACCESSTOKEN
    if ([string]::IsNullOrEmpty($token))
    {
        if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force }
        Import-Module CredentialManager
        $credential = Get-StoredCredential -Target "git:$Url"
        if ($null -eq $credential)
        {
            Throw "No Azure DevOps credentials found in credential store"
        }
        Write-Verbose "Use Azure DevOps Access Token from Windows Credential Store"
        $token = $credential.GetNetworkCredential().Password
    }
    return $token
}
<#
.SYNOPSIS
Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store
 
.DESCRIPTION
Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local.
 
.PARAMETER Url
Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany)
 
.Example
$cred = Get-AzureDevOpsCredential 'https://mycompany@dev.azure.com/mycompany')
#>

function Get-AzureDevOpsCredential($Url)
{
    $token = $env:SYSTEM_ACCESSTOKEN
    if ([string]::IsNullOrEmpty($token)) 
    {
        if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force }
        Import-Module CredentialManager
        $credential = Get-StoredCredential -Target "git:$Url"
        if ($null -eq $credential)
        {
            Throw "No Azure DevOps credentials found. It should be passed in via env:SYSTEM_ACCESSTOKEN."
        }
        Write-Verbose "Use Azure DevOps Access Token from Windows Credential Store"
    }
    else
    {
        Write-Verbose "Use Azure DevOps Access Token from Hosted Agent"
        $secureToken = $token | ConvertTo-SecureString -AsPlainText -Force
        $credential = New-Object System.Management.Automation.PSCredential(".", $secureToken)
    }
    return $credential
}
<#
.SYNOPSIS
Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts
 
.DESCRIPTION
Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts
 
.PARAMETER PackageSource
Azure DevOps packagesource name
 
.PARAMETER Modules
Array of modules to import
 
.PARAMETER Credential
Credentials to access feed
 
.PARAMETER Latest
Always import latest modules
 
.EXAMPLE
Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2
Import-AzureDevOpsModules -PackageSource 'myFeed' -Modules @('myModule') -Latest
#>

function Import-AzureDevOpsModules($PackageSource, $Modules, [System.Management.Automation.PSCredential]$Credential, [Switch]$Latest)
{
    foreach ($module in $Modules)
    {
        if (-not (Get-Module -ListAvailable -Name $module) -or $Latest.IsPresent)
        {
            Install-Module $module -Repository $PackageSource -Scope CurrentUser -Force -AllowClobber -Credential $Credential
        }
        else
        {
            Import-Module $module
        }
    }
}
<#
.SYNOPSIS
Publish the PowerShell Package to the Azure Devops Feed / Artifacts
 
.DESCRIPTION
Publish the PowerShell Package to the Azure Devops Feed / Artifacts. Depends on nuget.exe installed and in environment path.
 
Strategy:
- Register feed with nuget
- Register local temp feed to use Powershell Publish-Module command
- Publish locally created module to feed with nuget.exe
 
.PARAMETER ModuleName
Name of the PowerShell Module to publish
 
.PARAMETER ModulePath
Root path of the module
 
.PARAMETER Feedname
Name of the Azure DevOps feed
 
.PARAMETER FeedUrl
Url of the Azure DevOps feed
 
.PARAMETER AccessToken
Personal AccessToken used for Azure DevOps Feed push/publish
 
.Example
Publish-PackageToAzureDevOps -ModuleName 'MyModule' -ModulePath './Output' -Feedname 'MyFeed' -FeedUrl 'https://pkgs.dev.azure.com/mycompany/_packaging/MyFeed/nuget/v2' -AccessToken 'sasasasa'
 
#>

function Publish-PackageToAzureDevOps($ModuleName, $ModulePath = './Output', $Feedname, $FeedUrl, $AccessToken)
{
    $packageSource = $Feedname
    $packageFeedUrl = $FeedUrl

    $deployPath = Join-Path $ModulePath $ModuleName

    # register nuget feed
    $nuGet = (Get-Command 'nuget').Source
    &$nuGet sources Remove -Name $packageSource
    [string]$r = &$nuGet sources
    if (!($r.Contains($packageSource)))
    {
        # add as NuGet feed
        Write-Verbose "Add NuGet source"
        &$nuGet sources Add -Name $packageSource -Source $packageFeedUrl -username "." -password $AccessToken
    }

    # get module version
    $manifestFile = "./$ModuleName/$ModuleName.psd1"
    $manifest = Import-PowerShellDataFile -Path $manifestFile
    $version = $manifest.Item('ModuleVersion')
    if (!$version) { Throw "No module version found in $manifestFile" } else { Write-Host "$moduleName version: $version" }

    $tmpFeedPath = Join-Path ([System.IO.Path]::GetTempPath()) "$(New-Guid)-Localfeed"
    New-Item -Path $tmpFeedPath -ItemType Directory -ErrorAction Ignore -Force | Out-Null
    try 
    {
        # register temp feed for export package
        if (Get-PSRepository -Name LocalFeed -ErrorAction Ignore)
        {
            Unregister-PSRepository  -Name LocalFeed
        }
        Register-PSRepository -Name LocalFeed -SourceLocation $tmpFeedPath -PublishLocation $tmpFeedPath -InstallationPolicy Trusted

        # publish to temp feed
        $packageName = "$moduleName.$version.nupkg"
        $package = (Join-Path $tmpFeedPath $packageName)
        Write-Verbose "Publish Module $package"
        Publish-Module -Path $deployPath -Repository LocalFeed -Force -ErrorAction Ignore
        if (!(Test-Path $package))
        {
            Throw "Nuget package $package not created"
        }

        # publish package from tmp/local feed to PS feed
        Write-Verbose "Push package $packageName in $tmpFeedPath"
        Push-Location $tmpFeedPath
        try 
        {
            nuget push $packageName -source $packageSource -Apikey Az -NonInteractive
            if ($LastExitCode -ne 0)
            {
                Throw "Error pushing nuget package $packageName to feed $packageSource ($packageFeedUrl)"
            }
        }
        finally
        {
            Pop-Location    
        }
    }
    finally 
    {
        Remove-Item -Path $tmpFeedPath -Force -Recurse
    } 
}
<#
.SYNOPSIS
Registers a package source from AzureDevOps Feed / Artifacts
 
.DESCRIPTION
Registers a package source from AzureDevOps Feed /Artifacts. If already found removes reference first.
 
.PARAMETER Name
Name of package source
 
.PARAMETER Url
Url of package feed
 
.PARAMETER Credential
Credentials to access feed
 
.Example
Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2
#>

function Register-AzureDevOpsPackageSource($Name, $Url, [System.Management.Automation.PSCredential]$Credential)
{
    if ($Credential)
    {
        try 
        {
            Invoke-WebRequest -Uri $Url -Credential $Credential | Out-Null # check for access to artifacts with credential
        }
        catch {
            Throw "Register-AzureDevOpsPackageSource error for $Url : $($_.Exception.Message)"
        }
    }

    if (Get-PSRepository -Name $Name -ErrorAction Ignore) { Unregister-PSRepository -Name $Name }
    Register-PSRepository -Name $Name -SourceLocation $Url -InstallationPolicy Trusted -Credential $Credential
}