Az.DevOps.Blueprint.psm1

param
(
    [Parameter(Position=1, Mandatory=$false)]
    [ValidateNotNull()]
    $DevOpsUri = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI,

    [Parameter(Position=2, Mandatory=$false)]
    [ValidateNotNull()]
    $DevOpsProject = $env:SYSTEM_TEAMPROJECT,

    [Parameter(Position=7, Mandatory=$false)]
    [ValidateNotNull()]
    $context = (Get-AzContext)
)

function Get-AzDevOpsBlueprintParameters
{
    <#
    .SYNOPSIS
        Determines the Blueprint Parameters and creates two Variable Groups (required/not-required) containing the Variables required to match Parameters in the Blueprint.

    .DESCRIPTION
        The parameters in the Blueprint, follow a pattern where the first part of the parameter matches a Blueprint artifact. For example:
            keyvault_ad-domain-admin-user-password
            artifact | separator | parameter
            keyvault | _ | ad-domain-admin-user-password

    .PARAMETER InputPath
        A string containing the path to the Blueprint JSON File e.g.
        'C:\Repos\Blueprints\Small_ISO27001_Shared-Services'

    .EXAMPLE
        Get-AzDevOpsBlueprintParameters `
            -InputPath 'C:\Repos\Blueprints\Small_ISO27001_Shared-Services'

        Result:
            3 x Variable Groups:
             - BLUEPRINT_Parameters_Required
             - BLUEPRINT_Parameters_Not_Required
             - BLUEPRINT_Resource_Groups
    #>


    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [string]$InputPath,

        [Parameter(Mandatory=$true)]
        $DevOpsPAT,

        [Parameter(Mandatory=$false)]
        $DevOpsApiVersion = "5.0-preview.1"
    )

    try
    {
        # variables
        $DevOpsHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$DevOpsPAT"))}
        $getVarGrpsUri = "{0}{1}/_apis/distributedtask/variablegroups?api-version={2}" -f $DevOpsUri, $DevOpsProject, $DevOpsApiVersion
        $VariableGroups = (Invoke-RestMethod -Uri $getVarGrpsUri -Method GET -Header $DevOpsHeader).value
        $rawBlueprint = Get-Content -Path "$($InputPath)\Blueprint.json" | ConvertFrom-Json

        #region Resource Group Parameters
        $json = @{
            variables = @{}
            type = "Vsts"
            name = "BLUEPRINT_Resource_Groups"
            description = "These Variables in Azure DevOps map to Resource Group Parameters in the Blueprint"
        }

        Write-Output "Create these Variables in Azure DevOps for Resource Groups and assign values:"
        foreach ($param in $rawBlueprint.properties.resourceGroups.PSObject.Properties)
        {
            if ([string]::IsNullOrWhitespace($param.Value.Name))
            {
                $key = "RG_$($param.Value.metadata.DisplayName -Replace ' ','')"
                $value = "REPLACE THIS TEXT WITH A NAME FOR A RESOURCE GROUP"

                Write-Output "`tName: $($key)"
                Write-Output "`tBlueprint Resource Group: $($param.Name)`r`n"

                $json.variables.Add($key, $value)
            }
        }

        New-AzDevOpsBlueprintVariableGroup -Json $json -VariableGroups $VariableGroups -DevOpsPAT $DevOpsPAT
        #endregion

        #region Parameters that need Values
        $json = @{
            variables = @{}
            type = "Vsts"
            name = "BLUEPRINT_Parameters_Required"
            description = "These Variables in Azure DevOps map to Parameters in the Blueprint that need values"
        }

        Write-Output "Create these Variables in Azure DevOps and assign values:"
        foreach ($param in $rawBlueprint.properties.parameters.PSObject.Properties)
        {
            if ([string]::IsNullOrWhitespace($param.Value.defaultValue))
            {
                $key = "BP_$($param.Name -Replace '-','')"
                $value = Test-ParameterValue -Value $param.Value

                Write-Output "`tName: $($key)"
                Write-Output "`tType: $($param.Value.Type)`r`n"

                $json.variables.Add($key, $value)
            }
        }

        New-AzDevOpsBlueprintVariableGroup -Json $json -VariableGroups $VariableGroups -DevOpsPAT $DevOpsPAT
        #endregion

        #region Parameters that have Default Values and can be overridden
        $json = @{
            variables = @{}
            type = "Vsts"
            name = "BLUEPRINT_Parameters_Not_Required"
            description = "These Variables in Azure DevOps map to Parameters in the Blueprint that have default values and can be overridden."
        }

        Write-Output "These Variables have Default Values. Create any of these Variable, assign a value to override the Default Value:"
        foreach ($param in $rawBlueprint.properties.parameters.PSObject.Properties)
        {
            if (![string]::IsNullOrWhitespace($param.Value.defaultValue))
            {
                $key = "BP_$($param.Name -Replace '-','')"
                $value = Test-ParameterDefaultValue -Value $param.Value

                Write-Output "`tName: BP_$($param.Name -Replace '-','')"
                Write-Output "`tValue: $($value)"
                Write-Output "`tType: $($param.Value.Type)`r`n"

                $json.variables.Add($key, $value)
            }
        }

        New-AzDevOpsBlueprintVariableGroup -Json $json -VariableGroups $VariableGroups -DevOpsPAT $DevOpsPAT
    }
    catch
    {
        if ($_.ErrorDetails.Message) {$ErrDetails = $_.ErrorDetails.Message } else {$ErrDetails = $_}
        Get-StandardError -Exception $($ErrDetails)
    }
}

function Get-AzDevOpsBlueprintVariableGroups
{
    <#
    .SYNOPSIS
        Get Azure DevOps Variable Group or Groups

    .PARAMETER Name
        A string containing the name of a variable group to target. By default all Variable Groups are returned unless the a Variable Group is named

    .EXAMPLE
        Get all Variable Groups for an Azure DevOps Project
        Get-AzDevOpsBlueprintVariableGroups

    .EXAMPLE
        Get a Variable Group for an Azure DevOps Project
        Get-AzDevOpsBlueprintVariableGroups -Name TEST-Variable-Group
    #>


    [CmdletBinding(DefaultParameterSetName = 'AllScope')]
    param
    (
        [Parameter(Mandatory=$false, ParameterSetName = 'ByName')]
        [ValidateNotNull()]
        [string]$Name,

        [Parameter(Mandatory=$true)]
        $DevOpsPAT,

        [Parameter(Mandatory=$false)]
        $DevOpsApiVersion = "5.0-preview.1"
    )

    try
    {
        # variables
        $DevOpsHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$DevOpsPAT"))}
        $getVarGrpsUri = "{0}{1}/_apis/distributedtask/variablegroups?api-version={2}" -f $DevOpsUri, $DevOpsProject, $DevOpsApiVersion

        $results = (Invoke-RestMethod -Uri $getVarGrpsUri -Method GET -Headers $DevOpsHeader).value

        if ($Name)
        { $results = $results | Where-Object Name -eq $Name }

        return $results
    }
    catch
    {
        if ($_.ErrorDetails.Message) {$ErrDetails = $_.ErrorDetails.Message } else {$ErrDetails = $_}
        Get-StandardError -Exception $($ErrDetails)
    }
}

function Find-AzDevOpsBlueprintParameters
{
    <#
    .SYNOPSIS
        Matches different types of Blueprint parameters with Variables in Azure DevOps

    .DESCRIPTION
        Finds Blueprint Parameters in a Raw Blueprint and matches Resource Group, Parameters and Secure Parameters with Variables already defined in Azure DevOps

    .PARAMETER Type
        A string containing the Type of Parameter to match. There are two options
        1. ResourceGroup
        2. Parameters

    .PARAMETER RawBlueprint
        A PSCustomObject containing the raw data for a Blueprint.

    .PARAMETER Location
        A String containing the Location if a Resource Group parameter is required

    .EXAMPLE Match Blueprint Resource Group parameters
        Find-AzDevOpsBlueprintParameters `
            -Type ResourceGroups `
            -RawBlueprint (Get-Content -Raw '/Repos/Blueprints/Small_ISO27001_Shared-Services/Small_ISO27001_Shared-Services.json' | ConvertFrom-Json)

    .EXAMPLE Match Blueprint Parameters parameters
        Find-AzDevOpsBlueprintParameters `
            -Type Parameters `
            -RawBlueprint (Get-Content -Raw '/Repos/Blueprints/Small_ISO27001_Shared-Services/Small_ISO27001_Shared-Services.json' | ConvertFrom-Json)

    #>


    param
    (
        [Parameter(Mandatory=$True)]
        [ValidateNotNull()]
        [string]$Type,

        [Parameter(Mandatory=$True)]
        [ValidateNotNull()]
        [PSCustomObject]$RawBlueprint,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [string]$Location
    )

    switch ($Type)
    {
        "ResourceGroups"
        {
            $resourceGroups = @{}
            foreach ($rg in $rawBlueprint.properties.resourceGroups.PSObject.Properties)
            {
                foreach ($envVariable in (Get-ChildItem env: | Where-Object Name -like "RG_*"))
                {
                    $tmp = $rg.Value.metadata.DisplayName -replace " ", ""
                    $tmpEnv = $envVariable.Name -replace "RG_", ""
                    $tmpValue = $null
                    if ($tmp -like "*$($tmpEnv)*")
                    {
                        # Adding to resourceGroups
                        $resourceGroups.Add($rg.Name, @{name = $envVariable.value; location = $Location})
                    }
                }
            }

            if ($resourceGroups) {    return $resourceGroups    }
        }

        "Parameters"
        {
            $params = @{}
            foreach ($param in $rawBlueprint.properties.parameters.PSObject.Properties)
            {
                foreach ($envVariable in (Get-ChildItem env: | Where-Object Name -like "BP_*"))
                {
                    $tmp = $param.Name -replace "-", ""
                    $tmpEnv = $envVariable.Name -replace "BP_", ""
                    $tmpValue = $null
                    if ($tmp -like "*$($tmpEnv)*")
                    {
                        if ([string]::IsNullOrWhitespace($param.Value.defaultValue))
                        {
                            if ($param.Value.type -eq "array" -and [string]::IsNullOrWhitespace($envVariable.value))
                            {
                                $tmpValue = @()
                                $params.Add($param.Name, $tmpValue)
                            } elseif ($param.Value.type -eq "object" -and [string]::IsNullOrWhitespace($envVariable.value))
                            {
                                $tmpValue = @()
                                $params.Add($param.Name, $tmpValue)
                            } elseif ($param.Value.type -eq "object" -and $envVariable.value.getType().Name -eq "String")
                            {
                                $tmpObject = @{}
                                (ConvertFrom-Json $envVariable.value -ErrorAction Stop).psobject.properties | Foreach-Object { $tmpObject[$_.Name] = $_.Value }
                                $params.Add($param.Name, $tmpObject)
                            } elseif ($param.Value.type -eq "int" -and $envVariable.value.getType().Name -eq "String")
                            {
                                $params.Add($param.Name, [int]$envVariable.value)
                            } elseif ($param.Value.type -eq "string" -and [string]::IsNullOrWhitespace($envVariable.value))
                            {
                                $tmpValue = ""
                                $params.Add($param.Name, $tmpValue)
                            } elseif ($envVariable.Value.Contains('Microsoft.KeyVault/vaults'))
                            { continue } else
                            { $params.Add($param.Name, $envVariable.value)    }
                        } else
                        { $params.Add($param.Name, $envVariable.value)    }
                    }
                }

                # Checking for Variables that require a secret. These variables are defined as an Enviromment variable on the PowerShell Task and use a Azure DevOps Secret Variable
                foreach ($envVariable in (Get-ChildItem env: | Where-Object Name -like "BPS_*"))
                {
                    $tmp = $param.Name -replace "-", ""
                    $tmpEnv = $envVariable.Name -replace "BPS_", ""
                    $tmpValue = $null
                    if ($tmp -like "*$($tmpEnv)*")
                    {
                        if ([string]::IsNullOrWhitespace($param.Value.defaultValue))
                        {
                            # Adding to Params
                            $params.Add($param.Name, $envVariable.value)
                        }
                    }
                }
            }

            if ($params) {    return $params    }
        }

        "SecureParameters"
        {
            $secureParams = @{}
            foreach ($param in $rawBlueprint.properties.parameters.PSObject.Properties)
            {
                foreach ($envVariable in (Get-ChildItem env: | Where-Object Name -like "BP_*"))
                {
                    $tmp = $param.Name -replace "-", ""
                    $tmpEnv = $envVariable.Name -replace "BP_", ""
                    $tmpValue = $null
                    if ($tmp -like "*$($tmpEnv)*")
                    {
                        if ([string]::IsNullOrWhitespace($param.Value.defaultValue))
                        {
                            if ($envVariable.Value.Contains('Microsoft.KeyVault/vaults'))
                            {
                                $tmpValue = $envVariable.Value -split ","
                                $tmpValue = @{keyVaultId=$tmpValue[0];secretName=$tmpValue[1]}
                                $secureParams.Add($param.Name, $tmpValue)
                            }
                        }
                    }
                }
            }

            if ($secureParams) {    return $secureParams    }
        }
    }
}
function New-AzDevOpsBlueprintAssignment
{
    <#
    .SYNOPSIS
        Assigns a Blueprint in Azure

    .DESCRIPTION
        Assigns a version of a Blueprint that was published to a Management Group / Subscription

    .PARAMETER Blueprint
        An Object containing the Blueprint information.

    .PARAMETER AssignmentName
        A string containing the Assignment Name of the Blueprint

    .PARAMETER InputPath
        A string containing the path to the Blueprint JSON File e.g.
        'C:\Repos\Blueprints\Small_ISO27001_Shared-Services'

    .PARAMETER Location
        A string containing the azure region name to assign/create the resources for the Blueprint e.g. australiasoutheast

    .PARAMETER Version
        A string containing the Version of the Blueprint to be assigned. This value should be the Release Number generated by the Release pipeline e.g. 20190701.1 YYYYMMDD.R (YYYY - Year, MM = Month, DD = Day, R = Release number)

    .PARAMETER SubscriptionId
        A string containing the Subscription ID of the Subscription to import the Blueprint into. This parameter must be specified.

    .EXAMPLE Assign Blueprint to Management Group
        $Blueprint = Get-AzBlueprintAssignment -Name "Assignment-Small_ISO27001_Shared-Services" -Subscription "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        New-AzDevOpsBlueprintAssignment `
            -Blueprint $Blueprint `
            -AssignmentName "Assignment-Small_ISO27001_Shared-Services" `
            -InputPath "C:\Repos\Blueprints\Small_ISO27001_Shared-Services" `
            -Location "australiasoutheast" `
            -Version "20190901.001"
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'None')]
    param
    (
        [parameter(Mandatory=$true)]
        [object]$Blueprint,

        [parameter(Mandatory=$true)]
        [string]$AssignmentName,

        [Parameter(Mandatory=$True)]
        [ValidateNotNull()]
        [ValidateScript({Get-ChildItem -Path $_})]
        [string]$InputPath,

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

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

        [Parameter(Mandatory=$false)]
        [string]$SubscriptionId = $context.Subscription.Id
    )

    try
    {
        # Variables
        $rawBlueprint = Get-Content "$($InputPath)\Blueprint.json" | ConvertFrom-Json -ErrorAction Stop

        # Get Any old Assignments
        $oldAssignment = Get-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId -ErrorAction SilentlyContinue

        # Assign Blueprint
        Write-Output "`r`nAssigning Blueprint '$Blueprint.Name' using Version '$($Version)'....."
        Write-Output "Matching Variables to Blueprint Resource Groups and configuring Values....."

        $resourceGroups = Find-AzDevOpsBlueprintParameters -Type ResourceGroups -RawBlueprint $rawBlueprint -Location $Location

        if ($resourceGroups.count -ne 0)
        {
            Write-Output " Found Resource Group Parameters!"

            foreach ($resourceGroup in $resourceGroups)
            {
                Write-Output " Setting Resource Group Name to: $($resourceGroup.Values.Name)"
                Write-Output " Setting Resource location to: $($resourceGroup.Values.Location)`r`n"
            }
        }

        Write-Output "Matching Variables to Blueprint Parameters and configuring Values....."
        $params = Find-AzDevOpsBlueprintParameters -Type Parameters -RawBlueprint $rawBlueprint

        if ($params.count -ne 0)
        {
            Write-Output " Found Parameters!"

            foreach ($param in $params.Keys)
            {
                Write-Output " Setting Parameter: $($param)"
            }
        }

        Write-Output "Matching Variables to Blueprint Secure Parameters and configuring Values....."
        $secureParams = Find-AzDevOpsBlueprintParameters -Type SecureParameters -RawBlueprint $rawBlueprint

        if ($secureParams.count -ne 0)
        {
            Write-Output " Found Secure Parameters!"

            foreach ($param in $secureParams.Keys)
            {
                Write-Output " Setting Parameter: $($param)"
            }
        }

        if ($oldAssignment -and $PSCmdlet.ShouldProcess($AssignmentName, 'Proceed.')) {
            # if yes, *update* the assignment
            Write-Output "Updating existing assignment....."
            if ($resourceGroups.Count -ge 1)
            {
                if ($secureParams.Count -ge 1)
                {
                    Set-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -ResourceGroupParameter $resourceGroups -Parameter $params -SecureStringParameter $secureParams -ErrorAction Stop
                } else
                {
                    Set-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -ResourceGroupParameter $resourceGroups -Parameter $params -ErrorAction Stop
                }
            } else
            {
                if ($secureParams.Count -ge 1)
                {
                    Set-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -Parameter $params -SecureStringParameter $secureParams -ErrorAction Stop
                } else
                {
                    Set-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -Parameter $params -ErrorAction Stop
                }
            }
        } else {
            # if no assignment, create one
            Write-Output "Creating new assignment....."
            if ($resourceGroups.Count -ge 1)
            {
                if ($secureParams.Count -ge 1)
                {
                    New-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -ResourceGroupParameter $resourceGroups -Parameter $params -SecureStringParameter $secureParams -ErrorAction Stop
                } else
                {
                    New-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -ResourceGroupParameter $resourceGroups -Parameter $params -ErrorAction Stop
                }
            } else
            {
                if ($secureParams.Count -ge 1)
                {
                    New-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -Parameter $params -SecureStringParameter $secureParams -ErrorAction Stop
                } else
                {
                    New-AzBlueprintAssignment -Blueprint $Blueprint -Location $Location -SubscriptionId $SubscriptionId -Name $AssignmentName -Parameter $params -ErrorAction Stop
                }
            }
        }

        $assignment = Get-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId
        $counter = 0

        while (($assignment.ProvisioningState -ne "Succeeded") -and ($assignment.ProvisioningState -ne "Failed")) {
            Write-Output $assignment.ProvisioningState
            Start-Sleep -Seconds 5
            $assignment = Get-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId
            $counter++
        }

        if ($assignment.ProvisioningState -eq "Succeeded")
        {
            Write-Output "SUCCESS! Blueprint '$($Blueprint.Name) Version '$($Version)' has been Assigned"
        } elseif ($assignment.provisioningState -eq "Failed")
        {
            throw (Resolve-AzError -Last)
        } else {
            throw "Unhandled terminal state for assignment: {0}" -f $assignment.ProvisioningState
        }

    }
    catch
    {
        if ($_.ErrorDetails.Message) {$ErrDetails = $_.ErrorDetails.Message } else {$ErrDetails = $_}
        if ($_.Message) {$ErrDetails = $_.Message } else {$ErrDetails = $_}
        Get-StandardError -Exception $($ErrDetails)
    }
}

function Remove-AzDevOpsBlueprintAssignment
{
    <#
    .SYNOPSIS
        Removes a Blueprint Assignment in Azure

    .DESCRIPTION
        Removes a Blueprint that was Assigned to a Management Group / Subscription

    .PARAMETER AssignmentName
        A string containing the Assignment Name of the Blueprint

    .PARAMETER SubscriptionId
        A string containing the Subscription ID of the Subscription to import the Blueprint into. This parameter must be specified.

    .EXAMPLE Remove Assignment from a Subscription
        New-AzDevOpsBlueprintAssignment `
            -AssignmentName 'Assignment-Small_ISO27001_Shared-Services.json'
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'None')]
    param
    (
        [parameter(Mandatory=$true)]
        [string]$AssignmentName,

        [Parameter(Mandatory=$false)]
        [string]$SubscriptionId = $context.Subscription.Id,

        [Parameter(Mandatory=$false)]
        [bool]$Test = $false
    )

    try
    {
        # Get Assignment
        $Assignment = Get-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId -ErrorAction SilentlyContinue

        if ($Assignment -and $PSCmdlet.ShouldProcess($AssignmentName, 'Proceed.'))
        {
            $ResourceGroups = $Assignment.ResourceGroups
            Remove-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId -ErrorAction SilentlyContinue
            $Assignment = Get-AzBlueprintAssignment -Name $AssignmentName -Subscription $SubscriptionId -ErrorAction SilentlyContinue

            if ($Test -eq $true)
            {
                Write-Output "Removing Blueprint Assignment Resources"
                foreach ($item in $ResourceGroups)
                {
                    $null = Remove-AzResourceGroup -Name $item.ResourceGroup.Name -Force
                    Write-Output "SUCCESS! Removed Resource Group '$($item.ResourceGroup.Name)'"
                }
            }

            if (!$Assignment)
            {
                Write-Output "SUCCESS! Assignment '$($AssignmentName)' has been removed"
            } else {
                throw (Resolve-AzError -Last)
            }
        } else
        {
            Write-Output "Assignment '$($AssignmentName)' was not found."
        }
    }
    catch
    {
        if ($_.ErrorDetails.Message) {$ErrDetails = $_.ErrorDetails.Message } else {$ErrDetails = $_}
        if ($_.Message) {$ErrDetails = $_.Message } else {$ErrDetails = $_}
        Get-StandardError -Exception $($ErrDetails)
    }
}

function Test-ParameterValue
{
    <#
    .SYNOPSIS
        Checks the type of Blueprint Parameter

    .DESCRIPTION
        Determines the type of Blueprint Parameter and returns a description of what the Azure DevOps variable requires as a value.

    .PARAMETER Value
        A PSCustomObject containing the Blueprint Parameter information

    .EXAMPLE
        Test-ParameterValue `
            -Value [PSCustomObject]@{
                type = "string"
                metadata = [PSCustomObject]@{displayName = "app_tshirt-size (WebApp Template)"; description = "What T-Shirt size is required for the Web App"}
                defaultValue = "Medium"
                allowedValues = @("Small", "Medium", "Large")
            }
    #>


    param
    (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Value
    )

    if ($Value.type -eq "array")
    {
        $tmpValue = "Needs to be an Array e.g. value1, value2, etc."
    } elseif ($Value.type -eq "object")
    {
        $tmpValue = "Needs to be an Object, but as JSON e.g. '{`"key1`": `"value1`", `"key2`": `"value2`"}'"
    } elseif ($Value.type -eq "int")
    {
        $tmpValue = "Needs to be an Int i.e. Integer/Number e.g. 123 etc."
    } elseif ($Value.type -eq "secureString")
    {
        $tmpValue = "Needs to be ReferenceId to a password in a Key Vault e.g. /subscriptions/`$(subscriptionId)/resourceGroups/`$(keyVault.ResourceGroup)/providers/Microsoft.KeyVault/vaults/`$(keyVault),`$(BP_activedirectorydomainservices_addomainadminusername)."
    } elseif ($Value.type -eq "string")
    {
        $tmpValue = "Needs to be an String or Text e.g. This is Text etc."
    } else {
        $tmpValue = ""
    }

    return $tmpValue
}

function Test-ParameterDefaultValue
{
    <#
    .SYNOPSIS
        Checks the Default Value of Blueprint Parameter

    .DESCRIPTION
        Depending on the type of Blueprint Parameter, the Default Value is modified and returned as a string.

    .PARAMETER Value
        A PSCustomObject containing the Blueprint Parameter information

    .EXAMPLE
        Test-ParameterDefaultValue `
            -Value [PSCustomObject]@{
                type = "string"
                metadata = [PSCustomObject]@{displayName = "app_tshirt-size (WebApp Template)"; description = "What T-Shirt size is required for the Web App"}
                defaultValue = "Medium"
                allowedValues = @("Small", "Medium", "Large")
            }
    #>


    param
    (
        [Parameter(Mandatory=$true)]
        $Value
    )

    if ($Value.type -eq "array")
    {
        $tmpValue = $Value.defaultValue -join "`n"
        $tmpValue = $tmpValue -replace "`n", ","
    } elseif ($Value.type -eq "object")
    {
        $tmpValue = ($Value.defaultValue | ConvertTo-Json) -replace "`n", ""
        $tmpValue = $tmpValue -replace " ", ""
    } else {
        $tmpValue = $Value.defaultValue
    }

    return $tmpValue.toString()
}

function New-AzDevOpsBlueprintVariableGroup
{
    <#
    .SYNOPSIS
        Creates a Variable Group in a nominated Azure DevOps Project

    .DESCRIPTION
        The Variable Group created, contains the a group of variables detected from the Blueprint Parameters

    .PARAMETER Json
        A Hashtable containing Json information used to create the Variable Group

    .PARAMETER VariableGroups
        An Object containing all the Variable Groups in the Azure DevOps Project
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'None')]
    param
    (
        [Parameter(Mandatory=$true)]
        [hashtable]$Json,

        [Parameter(Mandatory=$true)]
        [object]$VariableGroups,

        [Parameter(Mandatory=$true)]
        $DevOpsPAT,

        [Parameter(Mandatory=$false)]
        $DevOpsApiVersion = "5.0-preview.1"
    )

    # Variables
    $DevOpsHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$DevOpsPAT"))}

    if ($json.variables.count -ge 1 -and $PSCmdlet.ShouldProcess($json, 'Proceed.'))
    {
        if ($Id = ($VariableGroups | Where-Object Name -eq $json.Name).id)
        {
            $uri = "{0}{1}/_apis/distributedtask/variablegroups/{2}?api-version={3}" -f $DevOpsUri, $DevOpsProject, $id, $DevOpsApiVersion
            $body = $json | ConvertTo-Json
            $null = Invoke-RestMethod -Uri $uri -Method PUT -Headers $DevOpsHeader -Body $body -ContentType "application/json" -ErrorAction Stop
            Write-Output "SUCCESS! Variable Group '$($json.Name)' has been updated in Azure DevOps"
        } else
        {
            $uri = "{0}{1}/_apis/distributedtask/variablegroups?api-version={2}" -f $DevOpsUri, $DevOpsProject, $DevOpsApiVersion
            $body = $json | ConvertTo-Json
            $null = Invoke-RestMethod -Uri $uri -Method POST -Headers $DevOpsHeader -Body $body -ContentType "application/json" -ErrorAction Stop
            Write-Output "SUCCESS! Variable Group '$($json.Name)' has been created in Azure DevOps"
        }
    }
}

function Get-StandardError
{
    <#
    .SYNOPSIS
        Error Message

    .DESCRIPTION
        Generates a standard error response

    .PARAMETER Exception
        Error Exception Object
    #>


    param
    (
        [Parameter(Mandatory=$true)]
        $Exception
    )

    Write-Output "An error occurred - please check rights or parameters for proper configuration and try again"
    Write-Output "======================================================================="
    Write-Output "Specific Error is: "
    Write-Output "$($Exception)"
    #Exit 1
}